kward 0.79.0 → 0.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +65 -1
- data/Gemfile.lock +2 -2
- data/README.md +31 -38
- data/doc/agent-tools.md +9 -10
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +69 -26
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +7 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +2 -2
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +2 -2
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +13 -4
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +10 -2
- data/doc/rpc.md +54 -31
- data/doc/sandboxing.md +22 -0
- data/doc/security.md +22 -7
- data/doc/session-management.md +2 -4
- data/doc/skills.md +2 -0
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +2 -2
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +44 -1
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +5 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/slash_commands.rb +1 -1
- data/lib/kward/cli/tabs.rb +103 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -0
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +51 -12
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +225 -20
- data/lib/kward/model/model_info.rb +49 -0
- data/lib/kward/model/payloads.rb +63 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +83 -13
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +99 -19
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +7 -7
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +219 -19
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +53 -12
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -5
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +60 -10
- data/lib/kward/rpc/session_manager.rb +109 -30
- data/lib/kward/session_store.rb +84 -24
- data/lib/kward/skills/registry.rb +16 -11
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +7 -2
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace_factory.rb +19 -2
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/setup.rb +4 -0
- metadata +26 -1
|
@@ -22,12 +22,15 @@ module Kward
|
|
|
22
22
|
|
|
23
23
|
# Writes the auth status output for the terminal CLI flow.
|
|
24
24
|
def print_auth_status
|
|
25
|
-
|
|
25
|
+
store = api_key_store
|
|
26
|
+
store.migrate_openrouter_config_key!
|
|
26
27
|
lines = ["#{colored("Auth Status", :green, :bold)}", ""]
|
|
27
28
|
lines << auth_status_line("OpenAI OAuth", File.exist?(OpenAIOAuth.default_auth_path), OpenAIOAuth.default_auth_path)
|
|
28
29
|
lines << auth_status_line("Anthropic OAuth", File.exist?(AnthropicOAuth.default_auth_path), AnthropicOAuth.default_auth_path)
|
|
29
30
|
lines << auth_status_line("GitHub OAuth", File.exist?(GithubOAuth.default_auth_path), GithubOAuth.default_auth_path)
|
|
30
|
-
|
|
31
|
+
ProviderCatalog.api_key_providers.each do |provider|
|
|
32
|
+
lines << auth_status_line("#{provider.name} API key", store.configured?(provider.id), store.path)
|
|
33
|
+
end
|
|
31
34
|
@prompt.say lines.join("\n")
|
|
32
35
|
end
|
|
33
36
|
|
|
@@ -45,7 +48,11 @@ module Kward
|
|
|
45
48
|
File.delete(path)
|
|
46
49
|
removed << path
|
|
47
50
|
end
|
|
48
|
-
|
|
51
|
+
store = api_key_store
|
|
52
|
+
store.migrate_openrouter_config_key!
|
|
53
|
+
ProviderCatalog.api_key_providers.each do |provider|
|
|
54
|
+
removed << "#{provider.name} API key" if store.delete(provider.id)
|
|
55
|
+
end
|
|
49
56
|
|
|
50
57
|
if removed.empty?
|
|
51
58
|
@prompt.say "No saved credentials found."
|
|
@@ -54,29 +61,81 @@ module Kward
|
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
|
|
57
|
-
def login(provider: nil, oauth: nil)
|
|
58
|
-
provider = provider
|
|
59
|
-
if provider
|
|
60
|
-
|
|
61
|
-
path = auth.login(prompt: @prompt)
|
|
62
|
-
@prompt.say("#{colored("Saved", :green, :bold)} OpenRouter API key to #{path}")
|
|
64
|
+
def login(provider: nil, oauth: nil, auth_method: nil)
|
|
65
|
+
provider = normalized_login_provider(provider)
|
|
66
|
+
if api_key_login?(provider, auth_method)
|
|
67
|
+
login_with_api_key(provider)
|
|
63
68
|
return
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
oauth ||= case provider
|
|
67
|
-
when "github" then GithubOAuth.new
|
|
68
|
-
when "anthropic"
|
|
72
|
+
when "github", "copilot" then GithubOAuth.new
|
|
73
|
+
when "anthropic" then AnthropicOAuth.new
|
|
69
74
|
else OpenAIOAuth.new
|
|
70
75
|
end
|
|
71
76
|
path = oauth.login(prompt: @prompt)
|
|
72
77
|
name = case provider
|
|
73
|
-
when "github" then "GitHub"
|
|
74
|
-
when "anthropic"
|
|
78
|
+
when "github", "copilot" then "GitHub"
|
|
79
|
+
when "anthropic" then "Anthropic"
|
|
75
80
|
else "OpenAI"
|
|
76
81
|
end
|
|
77
82
|
@prompt.say("#{colored("Saved", :green, :bold)} #{name} OAuth login to #{path}")
|
|
78
83
|
end
|
|
79
84
|
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def api_key_store
|
|
88
|
+
@api_key_store ||= APIKeyStore.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def normalized_login_provider(provider)
|
|
92
|
+
value = provider.to_s.downcase
|
|
93
|
+
return "openai" if value.empty?
|
|
94
|
+
return "anthropic" if ["anthropic", "claude"].include?(value)
|
|
95
|
+
return "copilot" if ["github", "copilot"].include?(value)
|
|
96
|
+
|
|
97
|
+
ProviderCatalog.fetch(value).id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def api_key_login?(provider, auth_method)
|
|
101
|
+
return true if auth_method == :api_key || auth_method == "api_key"
|
|
102
|
+
|
|
103
|
+
ProviderCatalog.fetch(provider).api_key? && !["openai", "anthropic", "copilot"].include?(provider)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def login_with_api_key(provider_id)
|
|
107
|
+
provider = ProviderCatalog.fetch(provider_id)
|
|
108
|
+
raise "#{provider.name} does not accept an API key" unless provider.api_key?
|
|
109
|
+
|
|
110
|
+
api_key = @prompt.ask("#{provider.name} API key:").to_s.strip
|
|
111
|
+
provider_config = provider_config_after_api_key_login(provider)
|
|
112
|
+
models = refresh_provider_models(provider, api_key)
|
|
113
|
+
path = api_key_store.store(provider.id, api_key)
|
|
114
|
+
ConfigFiles.update_config(provider_config) unless provider_config.empty?
|
|
115
|
+
@prompt.say("#{colored("Saved", :green, :bold)} #{provider.name} API key to #{path}")
|
|
116
|
+
@prompt.say("Loaded #{models.length} #{provider.name} model#{models.length == 1 ? "" : "s"}.") if models
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def refresh_provider_models(provider, api_key)
|
|
120
|
+
return unless ProviderCatalog.runtime(provider.id).automatic_model_discovery?
|
|
121
|
+
|
|
122
|
+
model_catalog(provider_id: provider.id, api_key: api_key).refresh
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def model_catalog(provider_id:, api_key:)
|
|
126
|
+
ModelCatalog.new(provider_id: provider_id, api_key: api_key)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def provider_config_after_api_key_login(provider)
|
|
130
|
+
return {} unless provider.id == "azure_openai"
|
|
131
|
+
|
|
132
|
+
AzureOpenAIConfig.new(
|
|
133
|
+
endpoint: @prompt.ask("Azure OpenAI endpoint:"),
|
|
134
|
+
deployment: @prompt.ask("Azure OpenAI deployment name:"),
|
|
135
|
+
api_version: @prompt.ask("Azure OpenAI API version:")
|
|
136
|
+
).to_config
|
|
137
|
+
end
|
|
138
|
+
|
|
80
139
|
end
|
|
81
140
|
end
|
|
82
141
|
end
|
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -59,6 +59,7 @@ module Kward
|
|
|
59
59
|
#{command.call("kward openrouter refresh")} Refresh cached OpenRouter models
|
|
60
60
|
#{command.call("kward pan")} Start Pan mode web UI
|
|
61
61
|
#{command.call("kward rpc")} Start the JSON-RPC backend
|
|
62
|
+
#{command.call("kward transport")} Manage transport plugins
|
|
62
63
|
|
|
63
64
|
#{heading.call("Commands")}
|
|
64
65
|
#{command.call("help")} Show this help
|
|
@@ -74,6 +75,7 @@ module Kward
|
|
|
74
75
|
#{command.call("openrouter refresh|list")} Refresh or list cached OpenRouter models
|
|
75
76
|
#{command.call("pan")} Start Pan mode web UI
|
|
76
77
|
#{command.call("rpc")} Run the JSON-RPC backend for UI clients
|
|
78
|
+
#{command.call("transport list|status|run")} Manage transport plugins
|
|
77
79
|
|
|
78
80
|
#{heading.call("Options")}
|
|
79
81
|
#{option.call("--working-directory=PATH")} Run Kward from PATH
|
|
@@ -163,6 +165,11 @@ module Kward
|
|
|
163
165
|
usage: "kward rpc",
|
|
164
166
|
description: "Start the JSON-RPC backend for trusted local UI clients.",
|
|
165
167
|
examples: ["kward rpc", "kward --working-directory ~/code/project rpc"]
|
|
168
|
+
},
|
|
169
|
+
"transport" => {
|
|
170
|
+
usage: "kward transport list|status [NAME]|run NAME [WORKSPACE]",
|
|
171
|
+
description: "Inspect or run trusted external transport plugins.",
|
|
172
|
+
examples: ["kward transport list", "kward transport status", "kward transport run telegram", "kward transport run telegram ~/code/project"]
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
end
|
data/lib/kward/cli/git.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "open3"
|
|
2
|
+
require "pathname"
|
|
2
3
|
|
|
3
4
|
# Namespace for the Kward CLI agent runtime.
|
|
4
5
|
module Kward
|
|
@@ -131,20 +132,40 @@ module Kward
|
|
|
131
132
|
{ path: path, staged: status[0] != " " && status[0] != "?", untracked: status == "??" }
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
def git_commit_for_agent(root, message:, paths: nil)
|
|
136
|
+
git_commit(root, message, paths: paths, stage_all: true, run_hooks: false)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def git_commit(root, message, paths: nil, stage_all: false, run_hooks: true)
|
|
140
|
+
if run_hooks
|
|
141
|
+
before = git_lifecycle_hook("git_commit_before", root: root, payload: { message: message.to_s })
|
|
142
|
+
return { success: false, output: "Declined: #{before.decision.message || "git commit denied"}" } if before.denied? || before.approval_required?
|
|
143
|
+
end
|
|
137
144
|
|
|
138
|
-
return git_commit_staged(root, message) if git_staged_changes?(root)
|
|
145
|
+
return git_commit_staged(root, message, run_hooks: run_hooks) if !stage_all && paths.nil? && git_staged_changes?(root)
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
add_arguments = ["git", "add", "--all"]
|
|
148
|
+
add_arguments.concat(["--", *validated_git_commit_paths(root, paths)]) if paths
|
|
149
|
+
add_output, add_status = Open3.capture2e(*add_arguments, chdir: root.to_s)
|
|
141
150
|
return { success: false, output: add_output } unless add_status.success?
|
|
142
151
|
|
|
143
|
-
git_commit_staged(root, message)
|
|
152
|
+
git_commit_staged(root, message, run_hooks: run_hooks)
|
|
144
153
|
rescue StandardError => e
|
|
145
154
|
{ success: false, output: e.message }
|
|
146
155
|
end
|
|
147
156
|
|
|
157
|
+
def validated_git_commit_paths(root, paths)
|
|
158
|
+
Array(paths).map do |path|
|
|
159
|
+
value = path.to_s
|
|
160
|
+
expanded = File.expand_path(value, root.to_s)
|
|
161
|
+
unless !value.empty? && !Pathname.new(value).absolute? && (expanded == root.to_s || expanded.start_with?("#{root}/"))
|
|
162
|
+
raise ArgumentError, "Git commit path must be workspace-relative: #{value}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
148
169
|
def git_staged_changes?(root)
|
|
149
170
|
_output, status = Open3.capture2e("git", "diff", "--cached", "--quiet", chdir: root.to_s)
|
|
150
171
|
!status.success?
|
|
@@ -152,10 +173,12 @@ module Kward
|
|
|
152
173
|
false
|
|
153
174
|
end
|
|
154
175
|
|
|
155
|
-
def git_commit_staged(root, message)
|
|
176
|
+
def git_commit_staged(root, message, run_hooks: true)
|
|
156
177
|
commit_output, commit_status = Open3.capture2e("git", "commit", "-m", message.to_s, chdir: root.to_s)
|
|
157
178
|
result = { success: commit_status.success?, output: commit_output }
|
|
158
|
-
|
|
179
|
+
if run_hooks
|
|
180
|
+
git_lifecycle_hook("git_commit_after", root: root, payload: { message: message.to_s, success: result[:success], output: result[:output] })
|
|
181
|
+
end
|
|
159
182
|
result
|
|
160
183
|
rescue StandardError => e
|
|
161
184
|
{ success: false, output: e.message }
|
|
@@ -104,7 +104,7 @@ module Kward
|
|
|
104
104
|
def prepare_memory_context(conversation, input)
|
|
105
105
|
Memory::TurnContext.apply(conversation: conversation, input: input)
|
|
106
106
|
rescue StandardError => e
|
|
107
|
-
|
|
107
|
+
emit_warning "Memory retrieval failed: #{e.message}"
|
|
108
108
|
nil
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -120,7 +120,7 @@ module Kward
|
|
|
120
120
|
|
|
121
121
|
summarize_memory(conversation, manager: manager)
|
|
122
122
|
rescue StandardError => e
|
|
123
|
-
|
|
123
|
+
emit_warning "Memory auto-summary failed: #{e.message}"
|
|
124
124
|
nil
|
|
125
125
|
end
|
|
126
126
|
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Kward
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def reload_plugins(conversation)
|
|
34
|
+
@prompt_templates = nil
|
|
34
35
|
@plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
35
36
|
@prompt.update_slash_commands(slash_command_entries) if @prompt.respond_to?(:update_slash_commands)
|
|
36
37
|
conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
|
|
@@ -15,6 +15,8 @@ module Kward
|
|
|
15
15
|
prompt_interface = load_prompt_interface
|
|
16
16
|
return unless prompt_interface
|
|
17
17
|
|
|
18
|
+
@interactive_warning_sink_active = true
|
|
19
|
+
ConfigFiles.warning_sink = interactive_warning_sink
|
|
18
20
|
@prompt = prompt_interface.new(
|
|
19
21
|
slash_commands: slash_command_entries,
|
|
20
22
|
overlay_settings: ConfigFiles.overlay_settings,
|
|
@@ -27,6 +29,7 @@ module Kward
|
|
|
27
29
|
banner_message: Kward::PromptInterface::BANNER_MESSAGE,
|
|
28
30
|
tab_keybindings: ConfigFiles.composer_tab_keybindings,
|
|
29
31
|
prompt_history: PromptHistory.new(cwd: current_workspace_root),
|
|
32
|
+
workspace_root: current_workspace_root,
|
|
30
33
|
editor_mode: ConfigFiles.editor_mode,
|
|
31
34
|
editor_mode_source: -> { ConfigFiles.editor_mode },
|
|
32
35
|
editor_auto_indent: ConfigFiles.editor_auto_indent?,
|
|
@@ -47,6 +50,28 @@ module Kward
|
|
|
47
50
|
else
|
|
48
51
|
@prompt.start
|
|
49
52
|
end
|
|
53
|
+
flush_interactive_warnings
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def interactive_warning_sink
|
|
57
|
+
@interactive_warning_sink ||= lambda do |message|
|
|
58
|
+
unless @interactive_warning_sink_active
|
|
59
|
+
warn message
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if prompt_interface?
|
|
64
|
+
runtime_output(message)
|
|
65
|
+
else
|
|
66
|
+
(@pending_interactive_warnings ||= []) << message
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def flush_interactive_warnings
|
|
72
|
+
warnings = @pending_interactive_warnings
|
|
73
|
+
@pending_interactive_warnings = []
|
|
74
|
+
Array(warnings).each { |message| runtime_output(message) }
|
|
50
75
|
end
|
|
51
76
|
|
|
52
77
|
def load_prompt_interface
|
|
@@ -67,6 +92,24 @@ module Kward
|
|
|
67
92
|
@prompt.respond_to?(:start_stream_block) && @prompt.respond_to?(:write_delta)
|
|
68
93
|
end
|
|
69
94
|
|
|
95
|
+
def emit_warning(message)
|
|
96
|
+
sink = ConfigFiles.warning_sink
|
|
97
|
+
sink ? sink.call(message) : warn(message)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def clear_interactive_warning_sink
|
|
101
|
+
ConfigFiles.warning_sink = nil
|
|
102
|
+
@interactive_warning_sink_active = false
|
|
103
|
+
@pending_interactive_warnings = nil
|
|
104
|
+
@interactive_warning_sink = nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def update_prompt_workspace_root(root)
|
|
108
|
+
return unless @prompt.respond_to?(:update_workspace_root)
|
|
109
|
+
|
|
110
|
+
@prompt.update_workspace_root(root, prompt_history: PromptHistory.new(cwd: root))
|
|
111
|
+
end
|
|
112
|
+
|
|
70
113
|
# Writes the startup info screen output for the terminal CLI flow.
|
|
71
114
|
def print_visual_banner
|
|
72
115
|
return unless @prompt.respond_to?(:print_visual_banner)
|
|
@@ -172,7 +215,7 @@ module Kward
|
|
|
172
215
|
context = plugin_context(current_footer_conversation, "")
|
|
173
216
|
renderer.call(context).to_s
|
|
174
217
|
rescue StandardError => e
|
|
175
|
-
|
|
218
|
+
emit_warning "Warning: Kward plugin footer error: #{e.message}"
|
|
176
219
|
""
|
|
177
220
|
end
|
|
178
221
|
end
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -272,8 +272,12 @@ module Kward
|
|
|
272
272
|
# Writes the block delta output for the terminal CLI flow.
|
|
273
273
|
def print_block_delta(label, delta)
|
|
274
274
|
if prompt_interface?
|
|
275
|
-
@prompt.
|
|
276
|
-
|
|
275
|
+
if @prompt.respond_to?(:write_stream_block)
|
|
276
|
+
@prompt.write_stream_block(label, delta, finish: false)
|
|
277
|
+
else
|
|
278
|
+
@prompt.start_stream_block(label)
|
|
279
|
+
@prompt.write_delta(delta)
|
|
280
|
+
end
|
|
277
281
|
else
|
|
278
282
|
start_stream_block(label)
|
|
279
283
|
print delta
|
|
@@ -60,6 +60,7 @@ module Kward
|
|
|
60
60
|
client: @client,
|
|
61
61
|
tool_registry: tool_registry,
|
|
62
62
|
conversation: conversation,
|
|
63
|
+
warning_sink: ConfigFiles.warning_sink,
|
|
63
64
|
hook_manager: hook_manager,
|
|
64
65
|
hook_context: hook_context
|
|
65
66
|
)
|
|
@@ -317,8 +318,8 @@ module Kward
|
|
|
317
318
|
@pending_inputs&.first.is_a?(Hash) && @pending_inputs.first[:tab_action]
|
|
318
319
|
end
|
|
319
320
|
|
|
320
|
-
def configured_workspace(root: current_workspace_root)
|
|
321
|
-
WorkspaceFactory.build(root: root, guardrails: workspace_guardrails_enabled?, config: safely_read_config.to_h)
|
|
321
|
+
def configured_workspace(root: current_workspace_root, strict: false)
|
|
322
|
+
WorkspaceFactory.build(root: root, guardrails: workspace_guardrails_enabled?, config: safely_read_config.to_h, strict: strict)
|
|
322
323
|
end
|
|
323
324
|
|
|
324
325
|
def workspace_guardrails_enabled?
|
|
@@ -401,6 +402,8 @@ module Kward
|
|
|
401
402
|
end
|
|
402
403
|
|
|
403
404
|
def current_workspace_root
|
|
405
|
+
conversation = active_tab&.agent&.conversation if respond_to?(:active_tab, true)
|
|
406
|
+
return conversation.workspace_root if conversation&.respond_to?(:workspace_root)
|
|
404
407
|
return @active_session.cwd.to_s unless @active_session&.cwd.to_s.empty?
|
|
405
408
|
return @working_directory if @working_directory
|
|
406
409
|
|
data/lib/kward/cli/sessions.rb
CHANGED
|
@@ -103,7 +103,7 @@ module Kward
|
|
|
103
103
|
@active_session = track_session(session_store.create)
|
|
104
104
|
reset_session_diff
|
|
105
105
|
cleanup_replaced_session(previous_session)
|
|
106
|
-
conversation = new_conversation(workspace_root:
|
|
106
|
+
conversation = new_conversation(workspace_root: current_workspace_root)
|
|
107
107
|
@active_session.attach(conversation)
|
|
108
108
|
run_lifecycle_hook("session_create", conversation: conversation, payload: { action: "create" })
|
|
109
109
|
update_assistant_prompt(conversation)
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -120,7 +120,10 @@ module Kward
|
|
|
120
120
|
|
|
121
121
|
def provider_choices
|
|
122
122
|
current = current_model_provider
|
|
123
|
-
|
|
123
|
+
configured_api_key_providers = ProviderCatalog.api_key_providers.filter_map do |provider|
|
|
124
|
+
provider.name if api_key_store.configured?(provider.id)
|
|
125
|
+
end
|
|
126
|
+
(["Codex", "Anthropic", "OpenRouter", "Copilot", "Local"] + configured_api_key_providers).uniq.map do |provider|
|
|
124
127
|
label = provider.dup
|
|
125
128
|
label += " (current)" if provider == current
|
|
126
129
|
label
|
|
@@ -135,7 +138,7 @@ module Kward
|
|
|
135
138
|
return "Copilot" if text.start_with?("copilot")
|
|
136
139
|
return "Local" if text.start_with?("local")
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
ProviderCatalog.find_by_name(selected)&.name
|
|
139
142
|
end
|
|
140
143
|
|
|
141
144
|
def configure_local_server_preset(conversation)
|
|
@@ -721,12 +724,15 @@ module Kward
|
|
|
721
724
|
return
|
|
722
725
|
end
|
|
723
726
|
|
|
724
|
-
|
|
727
|
+
method = selected_login_method(@prompt.select("How would you like to connect?", login_method_choices, title: "Login"))
|
|
728
|
+
return unless method
|
|
729
|
+
|
|
730
|
+
selected = @prompt.select(login_provider_prompt(method), login_provider_choices(method), title: "Login")
|
|
725
731
|
provider = selected_login_provider(selected)
|
|
726
732
|
return unless provider
|
|
727
733
|
|
|
728
734
|
run_busy_local_command_and_requeue(activity: "running") do
|
|
729
|
-
login(provider: provider)
|
|
735
|
+
login(provider: provider, auth_method: method)
|
|
730
736
|
reload_client_config
|
|
731
737
|
end
|
|
732
738
|
rescue StandardError => e
|
|
@@ -740,17 +746,40 @@ module Kward
|
|
|
740
746
|
end
|
|
741
747
|
|
|
742
748
|
models ||= normalized_available_models(conversation)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
749
|
+
provider_filter = nil
|
|
750
|
+
show_all = false
|
|
751
|
+
loop do
|
|
752
|
+
visible_models = provider_filter ? models.select { |model| model[:provider] == provider_filter } : models
|
|
753
|
+
visible_models = visible_models.select { |model| default_selectable_model?(model) } unless show_all
|
|
754
|
+
actions = ["Refresh model list", "Enter model ID manually", "Show all models", "Change provider"]
|
|
755
|
+
selected = @prompt.select("Default model", model_choices(visible_models, conversation) + actions, title: "Models", custom: true)
|
|
756
|
+
return unless selected
|
|
757
|
+
|
|
758
|
+
case selected.to_s
|
|
759
|
+
when "Refresh model list"
|
|
760
|
+
refreshed = @client.refresh_available_models(provider: provider_filter) if @client.respond_to?(:refresh_available_models)
|
|
761
|
+
models = normalized_available_models(conversation, models: refreshed)
|
|
762
|
+
when "Enter model ID manually"
|
|
763
|
+
provider = provider_filter || conversation&.provider || current_model_provider
|
|
764
|
+
model = @prompt.select("Model ID for #{provider}", [], title: "Models", custom: true)
|
|
765
|
+
return unless model
|
|
766
|
+
persist_model_selection(provider, model, conversation)
|
|
767
|
+
return
|
|
768
|
+
when "Show all models"
|
|
769
|
+
provider_filter = nil
|
|
770
|
+
show_all = true
|
|
771
|
+
when "Change provider"
|
|
772
|
+
selected_provider_name = selected_provider(@prompt.select("Provider", provider_choices, title: "Models"))
|
|
773
|
+
if selected_provider_name
|
|
774
|
+
provider_filter = selected_provider_name
|
|
775
|
+
show_all = false
|
|
776
|
+
end
|
|
777
|
+
else
|
|
778
|
+
provider, model = selected_model(selected, models)
|
|
779
|
+
persist_model_selection(provider, model, conversation)
|
|
780
|
+
return
|
|
781
|
+
end
|
|
782
|
+
end
|
|
754
783
|
rescue StandardError => e
|
|
755
784
|
runtime_output("Model error: #{e.message}")
|
|
756
785
|
end
|
|
@@ -784,23 +813,38 @@ module Kward
|
|
|
784
813
|
@prompt.respond_to?(:select)
|
|
785
814
|
end
|
|
786
815
|
|
|
787
|
-
def
|
|
788
|
-
["
|
|
816
|
+
def login_method_choices
|
|
817
|
+
["API key", "Subscription / OAuth"]
|
|
789
818
|
end
|
|
790
819
|
|
|
791
|
-
def
|
|
820
|
+
def selected_login_method(selected)
|
|
792
821
|
case selected.to_s.downcase
|
|
793
|
-
when /\
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
822
|
+
when /\Aapi key\z/ then :api_key
|
|
823
|
+
when /\Asubscription \/ oauth\z/ then :oauth
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def login_provider_prompt(method)
|
|
828
|
+
method == :api_key ? "Add an API key" : "Sign in with a subscription"
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def login_provider_choices(method)
|
|
832
|
+
if method == :api_key
|
|
833
|
+
ProviderCatalog.api_key_providers.map(&:name)
|
|
834
|
+
else
|
|
835
|
+
["Anthropic Claude", "ChatGPT", "GitHub Copilot"]
|
|
801
836
|
end
|
|
802
837
|
end
|
|
803
838
|
|
|
839
|
+
def selected_login_provider(selected)
|
|
840
|
+
value = selected.to_s.downcase
|
|
841
|
+
return "openai" if value == "openai" || value == "chatgpt"
|
|
842
|
+
return "anthropic" if value == "anthropic" || value == "anthropic claude"
|
|
843
|
+
return "copilot" if value == "github copilot"
|
|
844
|
+
|
|
845
|
+
ProviderCatalog.find_by_name(selected)&.id
|
|
846
|
+
end
|
|
847
|
+
|
|
804
848
|
def model_overlay_available?
|
|
805
849
|
@prompt.respond_to?(:select)
|
|
806
850
|
end
|
|
@@ -822,11 +866,11 @@ module Kward
|
|
|
822
866
|
values.find { |value| choice.to_s.downcase.start_with?(value) }
|
|
823
867
|
end
|
|
824
868
|
|
|
825
|
-
def normalized_available_models(conversation = current_footer_conversation)
|
|
869
|
+
def normalized_available_models(conversation = current_footer_conversation, models: nil)
|
|
826
870
|
current_provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
|
|
827
871
|
current_model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : nil)
|
|
828
872
|
current_reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil)
|
|
829
|
-
models
|
|
873
|
+
models ||= @client.respond_to?(:available_models) ? @client.available_models : []
|
|
830
874
|
ModelInfo.normalize_available(
|
|
831
875
|
models,
|
|
832
876
|
current_provider: current_provider,
|
|
@@ -846,17 +890,37 @@ module Kward
|
|
|
846
890
|
choices.empty? ? ["#{current_provider} #{current_model} (current)"] : choices.uniq
|
|
847
891
|
end
|
|
848
892
|
|
|
893
|
+
def default_selectable_model?(model)
|
|
894
|
+
return true if model[:current]
|
|
895
|
+
|
|
896
|
+
capabilities = Array(model[:supportedParameters]).map { |value| value.to_s.downcase }
|
|
897
|
+
return true if capabilities.empty?
|
|
898
|
+
|
|
899
|
+
capabilities.any? do |capability|
|
|
900
|
+
["tools", "toolchoice", "functioncalling", "generatecontent", "streamgeneratecontent"].include?(capability.delete("_-"))
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
849
904
|
def selected_model(selected, models)
|
|
850
905
|
text = selected.to_s.sub(/ \(current\)\z/, "").strip
|
|
851
906
|
known = models.find { |model| "#{model[:provider]} #{model[:id]}" == text }
|
|
852
907
|
return [known[:provider], known[:id]] if known
|
|
853
908
|
|
|
854
909
|
provider, model = text.split(/\s+/, 2)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
910
|
+
known_provider = ["Codex", "Anthropic", "OpenRouter", "Copilot", "Local"].include?(provider) || ProviderCatalog.find_by_name(provider)
|
|
911
|
+
return [provider, model.strip] if known_provider && !model.to_s.strip.empty?
|
|
912
|
+
|
|
913
|
+
[current_model_provider, text]
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def persist_model_selection(provider, model, conversation)
|
|
917
|
+
model = model.to_s.strip
|
|
918
|
+
raise "Model must be a non-empty string" if model.empty?
|
|
919
|
+
|
|
920
|
+
ConfigFiles.update_config(ModelInfo.config_values_for_selection(provider, model))
|
|
921
|
+
reload_client_config
|
|
922
|
+
refresh_conversation_runtime(conversation)
|
|
923
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
860
924
|
end
|
|
861
925
|
|
|
862
926
|
REASONING_CONFIG_DEBOUNCE_SECONDS = 0.5
|
|
@@ -345,7 +345,7 @@ module Kward
|
|
|
345
345
|
percent = ((reserve_tokens.to_f / context_window.to_i) * 100).round(1)
|
|
346
346
|
"Auto-compaction reserve: #{reserve_tokens} tokens (#{percent}% of #{context_window})"
|
|
347
347
|
rescue StandardError => e
|
|
348
|
-
|
|
348
|
+
emit_warning "Auto-compaction status unavailable: #{e.message}"
|
|
349
349
|
nil
|
|
350
350
|
end
|
|
351
351
|
|