kward 0.77.0 → 0.79.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/.github/workflows/ci.yml +7 -2
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +50 -0
- data/Gemfile.lock +2 -2
- data/README.md +5 -2
- data/doc/agent-tools.md +1 -1
- data/doc/code-search.md +9 -6
- data/doc/configuration.md +106 -1
- data/doc/extensibility.md +2 -0
- data/doc/getting-started.md +1 -1
- data/doc/local-models.md +130 -0
- data/doc/permissions.md +180 -0
- data/doc/plugins.md +58 -0
- data/doc/releasing.md +1 -1
- data/doc/rpc.md +73 -1
- data/doc/sandboxing.md +120 -0
- data/doc/security.md +15 -5
- data/doc/skills.md +10 -0
- data/doc/tabs.md +4 -3
- data/doc/web-search.md +4 -2
- data/doc/workspace-tools.md +3 -3
- data/kward.gemspec +1 -1
- data/lib/kward/auth/anthropic_oauth.rb +2 -2
- data/lib/kward/auth/github_oauth.rb +3 -3
- data/lib/kward/auth/oauth_helpers.rb +4 -2
- data/lib/kward/auth/openai_oauth.rb +2 -1
- data/lib/kward/cli/doctor.rb +21 -0
- data/lib/kward/cli/plugins.rb +22 -0
- data/lib/kward/cli/rendering.rb +7 -1
- data/lib/kward/cli/runtime_helpers.rb +15 -1
- data/lib/kward/cli/settings.rb +66 -4
- data/lib/kward/cli/slash_commands.rb +109 -1
- data/lib/kward/cli/tabs.rb +140 -34
- data/lib/kward/cli.rb +31 -10
- data/lib/kward/config_files.rb +90 -1
- data/lib/kward/conversation.rb +14 -1
- data/lib/kward/hooks/http_handler.rb +2 -1
- data/lib/kward/http.rb +18 -0
- data/lib/kward/local_command_runner.rb +2 -2
- data/lib/kward/model/client.rb +173 -15
- data/lib/kward/model/model_info.rb +11 -1
- data/lib/kward/model/payloads.rb +7 -1
- data/lib/kward/model/stream_parser.rb +58 -26
- data/lib/kward/openrouter_model_cache.rb +2 -1
- data/lib/kward/pan/index.html.erb +50 -0
- data/lib/kward/pan/server.rb +49 -2
- data/lib/kward/permissions/policy.rb +171 -0
- data/lib/kward/plugin_registry.rb +54 -2
- data/lib/kward/prompt_interface/approval_prompt.rb +62 -0
- data/lib/kward/prompt_interface/editor/controller.rb +36 -3
- data/lib/kward/prompt_interface/question_prompt.rb +12 -3
- data/lib/kward/prompt_interface.rb +20 -0
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts.rb +16 -5
- data/lib/kward/rpc/plugin_chat_manager.rb +302 -0
- data/lib/kward/rpc/server.rb +76 -3
- data/lib/kward/rpc/session_manager.rb +43 -8
- data/lib/kward/rpc/transcript_normalizer.rb +14 -5
- data/lib/kward/sandbox/capabilities.rb +39 -0
- data/lib/kward/sandbox/command_runner.rb +28 -0
- data/lib/kward/sandbox/environment.rb +24 -0
- data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
- data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
- data/lib/kward/sandbox/passthrough_runner.rb +13 -0
- data/lib/kward/sandbox/policy.rb +74 -0
- data/lib/kward/sandbox/runner_factory.rb +55 -0
- data/lib/kward/sandbox/unavailable_runner.rb +21 -0
- data/lib/kward/sandbox.rb +9 -0
- data/lib/kward/session_store.rb +26 -0
- data/lib/kward/skills/capture.rb +144 -0
- data/lib/kward/starter_pack_installer.rb +3 -1
- data/lib/kward/tab_driver.rb +87 -0
- data/lib/kward/tab_store.rb +74 -12
- data/lib/kward/tools/code_search.rb +8 -2
- data/lib/kward/tools/fetch_content.rb +4 -2
- data/lib/kward/tools/fetch_raw.rb +3 -1
- data/lib/kward/tools/registry.rb +56 -8
- data/lib/kward/tools/search/code.rb +46 -12
- data/lib/kward/tools/search/web.rb +60 -17
- data/lib/kward/tools/search/web_fetch.rb +206 -38
- data/lib/kward/tools/web_search.rb +3 -1
- data/lib/kward/update_check.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +18 -3
- data/lib/kward/workspace_factory.rb +17 -0
- data/templates/default/fulldoc/html/js/kward.js +1 -0
- data/templates/default/kward_navigation.rb +5 -2
- data/templates/default/layout/html/layout.erb +2 -0
- data/templates/default/layout/html/setup.rb +2 -0
- metadata +22 -2
|
@@ -202,8 +202,8 @@ module Kward
|
|
|
202
202
|
parse_successful_json(response, "Anthropic OAuth token exchange")
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
def post_json(uri, params)
|
|
206
|
-
super(uri, params, headers: { "Accept" => "application/json" })
|
|
205
|
+
def post_json(uri, params = nil, **keyword_params)
|
|
206
|
+
super(uri, params, headers: { "Accept" => "application/json" }, **keyword_params)
|
|
207
207
|
end
|
|
208
208
|
end
|
|
209
209
|
end
|
|
@@ -4,6 +4,7 @@ require "time"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require_relative "file"
|
|
6
6
|
require_relative "../config_files"
|
|
7
|
+
require_relative "../http"
|
|
7
8
|
|
|
8
9
|
# Namespace for the Kward CLI agent runtime.
|
|
9
10
|
module Kward
|
|
@@ -15,7 +16,6 @@ module Kward
|
|
|
15
16
|
DEFAULT_SCOPE = "read:user"
|
|
16
17
|
DEFAULT_CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
|
17
18
|
COPILOT_HEADERS = {
|
|
18
|
-
"User-Agent" => "GitHubCopilotChat/0.35.0",
|
|
19
19
|
"Editor-Version" => "vscode/1.107.0",
|
|
20
20
|
"Editor-Plugin-Version" => "copilot-chat/0.35.0",
|
|
21
21
|
"Copilot-Integration-Id" => "vscode-chat"
|
|
@@ -150,7 +150,7 @@ module Kward
|
|
|
150
150
|
end
|
|
151
151
|
|
|
152
152
|
def post_form(uri, params)
|
|
153
|
-
request = Net::HTTP::Post.new(uri)
|
|
153
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
154
154
|
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
155
155
|
request["Accept"] = "application/json"
|
|
156
156
|
request.body = URI.encode_www_form(params)
|
|
@@ -160,7 +160,7 @@ module Kward
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def get_json(uri, headers = {})
|
|
163
|
-
request = Net::HTTP::Get.new(uri)
|
|
163
|
+
request = Http.apply_user_agent(Net::HTTP::Get.new(uri))
|
|
164
164
|
request["Accept"] = "application/json"
|
|
165
165
|
COPILOT_HEADERS.merge(headers).each { |key, value| request[key] = value }
|
|
166
166
|
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "socket"
|
|
7
7
|
require "time"
|
|
8
8
|
require "uri"
|
|
9
|
+
require_relative "../http"
|
|
9
10
|
|
|
10
11
|
# Namespace for the Kward CLI agent runtime.
|
|
11
12
|
module Kward
|
|
@@ -58,8 +59,9 @@ module Kward
|
|
|
58
59
|
socket&.close
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def post_json(uri, params, headers: {})
|
|
62
|
-
|
|
62
|
+
def post_json(uri, params = nil, headers: {}, **keyword_params)
|
|
63
|
+
params = (params || {}).merge(keyword_params)
|
|
64
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
63
65
|
request["Content-Type"] = "application/json"
|
|
64
66
|
headers.each { |key, value| request[key] = value }
|
|
65
67
|
request.body = JSON.dump(params)
|
|
@@ -5,6 +5,7 @@ require "time"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
require_relative "file"
|
|
7
7
|
require_relative "oauth_helpers"
|
|
8
|
+
require_relative "../http"
|
|
8
9
|
|
|
9
10
|
# Namespace for the Kward CLI agent runtime.
|
|
10
11
|
module Kward
|
|
@@ -215,7 +216,7 @@ module Kward
|
|
|
215
216
|
end
|
|
216
217
|
|
|
217
218
|
def post_form(uri, params)
|
|
218
|
-
request = Net::HTTP::Post.new(uri)
|
|
219
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
219
220
|
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
220
221
|
request.body = URI.encode_www_form(params)
|
|
221
222
|
|
data/lib/kward/cli/doctor.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Kward
|
|
|
25
25
|
doctor_directory_check("Session directory", SessionStore.new(cwd: current_workspace_root).session_dir, create: true),
|
|
26
26
|
doctor_workspace_check,
|
|
27
27
|
doctor_model_check,
|
|
28
|
+
doctor_local_endpoint_check(config),
|
|
28
29
|
doctor_auth_check(config),
|
|
29
30
|
doctor_pan_check(config_result),
|
|
30
31
|
{ status: :ok, label: "Color", message: @color_enabled ? "enabled" : "disabled" }
|
|
@@ -89,14 +90,34 @@ module Kward
|
|
|
89
90
|
{ status: :warning, label: "Model", message: e.message }
|
|
90
91
|
end
|
|
91
92
|
|
|
93
|
+
def doctor_local_endpoint_check(config)
|
|
94
|
+
provider = ENV["KWARD_PROVIDER"].to_s.strip
|
|
95
|
+
provider = config["provider"].to_s.strip if provider.empty?
|
|
96
|
+
return { status: :ok, label: "Local endpoint", message: "not selected" } unless provider.casecmp?("local")
|
|
97
|
+
|
|
98
|
+
backend = ENV["KWARD_LOCAL_BACKEND"].to_s.strip
|
|
99
|
+
backend = config["local_backend"].to_s.strip if backend.empty?
|
|
100
|
+
defaults = Kward::Client::LOCAL_BASE_URLS
|
|
101
|
+
url = ENV["KWARD_LOCAL_BASE_URL"].to_s.strip
|
|
102
|
+
url = config["local_base_url"].to_s.strip if url.empty?
|
|
103
|
+
url = defaults.fetch(backend.empty? ? "ollama" : backend, defaults.fetch("ollama")) if url.empty?
|
|
104
|
+
uri = URI.parse(url)
|
|
105
|
+
loopback = ["127.0.0.1", "::1", "localhost"].include?(uri.host)
|
|
106
|
+
{ status: loopback ? :ok : :warning, label: "Local endpoint", message: "#{url}#{loopback ? " (loopback)" : " (non-loopback)"}" }
|
|
107
|
+
rescue URI::InvalidURIError
|
|
108
|
+
{ status: :error, label: "Local endpoint", message: "invalid URL: #{url}" }
|
|
109
|
+
end
|
|
110
|
+
|
|
92
111
|
def doctor_auth_check(config)
|
|
93
112
|
openai_auth = OpenAIOAuth.default_auth_path
|
|
94
113
|
github_auth = GithubOAuth.default_auth_path
|
|
95
114
|
has_openrouter = !config.to_h["openrouter_api_key"].to_s.empty? || !ENV["OPENROUTER_API_KEY"].to_s.empty?
|
|
115
|
+
local_provider = @client.respond_to?(:current_provider) && @client.current_provider == "Local"
|
|
96
116
|
paths = []
|
|
97
117
|
paths << "OpenAI OAuth" if File.exist?(openai_auth)
|
|
98
118
|
paths << "GitHub OAuth" if File.exist?(github_auth)
|
|
99
119
|
paths << "OpenRouter API key" if has_openrouter
|
|
120
|
+
paths << "Local endpoint (no authentication required)" if local_provider
|
|
100
121
|
return { status: :ok, label: "Auth", message: paths.join(", ") } if paths.any?
|
|
101
122
|
|
|
102
123
|
{ status: :warning, label: "Auth", message: "no saved credentials found; run `kward login`" }
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -102,6 +102,17 @@ module Kward
|
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def update_plugin_tab_slash_commands(tab)
|
|
106
|
+
return unless @prompt.respond_to?(:update_slash_commands)
|
|
107
|
+
|
|
108
|
+
entries = if tab&.driver&.respond_to?(:slash_command_entries)
|
|
109
|
+
tab.driver.slash_command_entries
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
@prompt.update_slash_commands(slash_command_entries + Array(entries))
|
|
114
|
+
end
|
|
115
|
+
|
|
105
116
|
def slash_command_entries
|
|
106
117
|
prompt_entries = prompt_templates.map do |template|
|
|
107
118
|
{
|
|
@@ -192,6 +203,17 @@ module Kward
|
|
|
192
203
|
plugin_registry.notify_transcript_event(event, plugin_context(conversation, ""))
|
|
193
204
|
end
|
|
194
205
|
|
|
206
|
+
def notify_plugin_tab_transcript_event(event, driver)
|
|
207
|
+
return if plugin_registry.transcript_event_handlers.empty?
|
|
208
|
+
|
|
209
|
+
context = PluginRegistry::Context.new(
|
|
210
|
+
conversation: driver,
|
|
211
|
+
workspace_root: current_workspace_root,
|
|
212
|
+
say_callback: lambda { |message| runtime_output(message) }
|
|
213
|
+
)
|
|
214
|
+
plugin_registry.notify_transcript_event(event, context)
|
|
215
|
+
end
|
|
216
|
+
|
|
195
217
|
end
|
|
196
218
|
end
|
|
197
219
|
end
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -7,9 +7,15 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def render_conversation_transcript(conversation)
|
|
10
|
+
render_transcript_messages(conversation.messages)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Renders a transcript supplied by either a Kward conversation or a
|
|
14
|
+
# plugin-owned tab driver.
|
|
15
|
+
def render_transcript_messages(messages)
|
|
10
16
|
tool_calls_by_id = {}
|
|
11
17
|
@prompt.say("\n#{colored("Transcript", :gray, :bold)}\n")
|
|
12
|
-
|
|
18
|
+
Array(messages).each do |message|
|
|
13
19
|
role = message_role(message)
|
|
14
20
|
next if role == "system"
|
|
15
21
|
|
|
@@ -50,6 +50,7 @@ module Kward
|
|
|
50
50
|
tool_registry = ToolRegistry.new(
|
|
51
51
|
workspace: workspace,
|
|
52
52
|
prompt: @prompt,
|
|
53
|
+
tool_approval: interactive_tool_approval_callback,
|
|
53
54
|
hook_manager: hook_manager,
|
|
54
55
|
hook_context: hook_context
|
|
55
56
|
)
|
|
@@ -64,6 +65,19 @@ module Kward
|
|
|
64
65
|
)
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
def interactive_tool_approval_callback
|
|
69
|
+
return nil unless @prompt.respond_to?(:ask_tool_approval)
|
|
70
|
+
return nil unless ConfigFiles.permission_policy(safely_read_config.to_h).enabled?
|
|
71
|
+
|
|
72
|
+
lambda do |tool_call:, name:, args:, cancellation:|
|
|
73
|
+
@prompt.ask_tool_approval(
|
|
74
|
+
tool_name: name,
|
|
75
|
+
args: args,
|
|
76
|
+
reason: args["hook_message"] || args[:hook_message]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
67
81
|
def handle_interactive_shell_command(input, agent)
|
|
68
82
|
command = input.to_s.sub(/\A!\s*/, "")
|
|
69
83
|
if command.strip.empty?
|
|
@@ -304,7 +318,7 @@ module Kward
|
|
|
304
318
|
end
|
|
305
319
|
|
|
306
320
|
def configured_workspace(root: current_workspace_root)
|
|
307
|
-
|
|
321
|
+
WorkspaceFactory.build(root: root, guardrails: workspace_guardrails_enabled?, config: safely_read_config.to_h)
|
|
308
322
|
end
|
|
309
323
|
|
|
310
324
|
def workspace_guardrails_enabled?
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -88,7 +88,7 @@ module Kward
|
|
|
88
88
|
def configure_model_settings(conversation)
|
|
89
89
|
initial_index = 0
|
|
90
90
|
loop do
|
|
91
|
-
selected, selected_index = select_settings_menu_item("Model & Reasoning", ["Provider", "Default model", "Reasoning effort", "Back"], initial_index)
|
|
91
|
+
selected, selected_index = select_settings_menu_item("Model & Reasoning", ["Provider", "Default model", "Local server preset", "Reasoning effort", "Back"], initial_index)
|
|
92
92
|
break unless selected
|
|
93
93
|
|
|
94
94
|
initial_index = selected_index if selected_index
|
|
@@ -97,6 +97,8 @@ module Kward
|
|
|
97
97
|
configure_provider(conversation)
|
|
98
98
|
when /\Adefault model/
|
|
99
99
|
configure_model(conversation)
|
|
100
|
+
when /\Alocal server preset/
|
|
101
|
+
configure_local_server_preset(conversation)
|
|
100
102
|
when /\Areasoning effort/
|
|
101
103
|
configure_reasoning(conversation)
|
|
102
104
|
else
|
|
@@ -118,7 +120,7 @@ module Kward
|
|
|
118
120
|
|
|
119
121
|
def provider_choices
|
|
120
122
|
current = current_model_provider
|
|
121
|
-
["Codex", "Anthropic", "OpenRouter", "Copilot"].map do |provider|
|
|
123
|
+
["Codex", "Anthropic", "OpenRouter", "Copilot", "Local"].map do |provider|
|
|
122
124
|
label = provider.dup
|
|
123
125
|
label += " (current)" if provider == current
|
|
124
126
|
label
|
|
@@ -131,10 +133,39 @@ module Kward
|
|
|
131
133
|
return "Anthropic" if text.start_with?("anthropic") || text.start_with?("claude")
|
|
132
134
|
return "OpenRouter" if text.start_with?("openrouter")
|
|
133
135
|
return "Copilot" if text.start_with?("copilot")
|
|
136
|
+
return "Local" if text.start_with?("local")
|
|
134
137
|
|
|
135
138
|
nil
|
|
136
139
|
end
|
|
137
140
|
|
|
141
|
+
def configure_local_server_preset(conversation)
|
|
142
|
+
choices = [
|
|
143
|
+
"Ollama (http://127.0.0.1:11434/v1)",
|
|
144
|
+
"LM Studio (http://127.0.0.1:1234/v1)",
|
|
145
|
+
"llama.cpp (http://127.0.0.1:8080/v1)",
|
|
146
|
+
"Custom endpoint (edit config)",
|
|
147
|
+
"Back"
|
|
148
|
+
]
|
|
149
|
+
selected = @prompt.select("Local server preset", choices, title: "Settings")
|
|
150
|
+
return unless selected
|
|
151
|
+
|
|
152
|
+
backend, url = case selected.to_s
|
|
153
|
+
when /\AOllama/ then ["ollama", "http://127.0.0.1:11434/v1"]
|
|
154
|
+
when /\ALM Studio/ then ["lm_studio", "http://127.0.0.1:1234/v1"]
|
|
155
|
+
when /\Allama\.cpp/ then ["llama_cpp", "http://127.0.0.1:8080/v1"]
|
|
156
|
+
when /\ACustom endpoint/
|
|
157
|
+
runtime_output("Set local_base_url in #{ConfigFiles.config_path}.")
|
|
158
|
+
return
|
|
159
|
+
else
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
ConfigFiles.update_config("local_backend" => backend, "local_base_url" => url)
|
|
164
|
+
reload_client_config
|
|
165
|
+
refresh_conversation_runtime(conversation)
|
|
166
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
167
|
+
end
|
|
168
|
+
|
|
138
169
|
def configure_account_settings
|
|
139
170
|
initial_index = 0
|
|
140
171
|
loop do
|
|
@@ -524,6 +555,10 @@ module Kward
|
|
|
524
555
|
case selected.to_s.downcase
|
|
525
556
|
when /\Adefault persona/
|
|
526
557
|
configure_default_persona(conversation)
|
|
558
|
+
when /\Aenable global principles/, /\Adisable global principles/
|
|
559
|
+
toggle_global_principles(conversation)
|
|
560
|
+
when /\Aglobal principles \(ignored/
|
|
561
|
+
runtime_output("Global principles are ignored while a replacement system prompt is configured.")
|
|
527
562
|
when /\Aactive instructions/
|
|
528
563
|
show_active_instructions_summary(conversation)
|
|
529
564
|
else
|
|
@@ -533,13 +568,31 @@ module Kward
|
|
|
533
568
|
end
|
|
534
569
|
|
|
535
570
|
def personalization_setting_choices(conversation)
|
|
571
|
+
config = safely_read_config.to_h
|
|
572
|
+
replacement = ConfigFiles.system_prompt_file_path(config)
|
|
573
|
+
principles = if replacement
|
|
574
|
+
"Global principles (ignored by replacement)"
|
|
575
|
+
elsif ConfigFiles.include_config_principles?(config)
|
|
576
|
+
"Disable global principles"
|
|
577
|
+
else
|
|
578
|
+
"Enable global principles"
|
|
579
|
+
end
|
|
536
580
|
[
|
|
537
581
|
"Default persona (#{default_persona_label})",
|
|
582
|
+
principles,
|
|
538
583
|
"Active instructions summary",
|
|
539
584
|
"Back"
|
|
540
585
|
]
|
|
541
586
|
end
|
|
542
587
|
|
|
588
|
+
def toggle_global_principles(conversation)
|
|
589
|
+
config = safely_read_config.to_h
|
|
590
|
+
settings = config["system_prompt"].is_a?(Hash) ? config["system_prompt"] : {}
|
|
591
|
+
ConfigFiles.update_config("system_prompt" => settings.merge("include_principles" => !ConfigFiles.include_config_principles?(config)))
|
|
592
|
+
conversation&.refresh_system_message!
|
|
593
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
594
|
+
end
|
|
595
|
+
|
|
543
596
|
def default_persona_label
|
|
544
597
|
personas = safely_read_config.to_h["personas"]
|
|
545
598
|
value = personas.is_a?(Hash) ? personas["default"] : nil
|
|
@@ -569,9 +622,18 @@ module Kward
|
|
|
569
622
|
|
|
570
623
|
def show_active_instructions_summary(conversation)
|
|
571
624
|
label = ConfigFiles.active_persona_label(workspace_root: current_workspace_root, model: current_model_id, config: safely_read_config.to_h)
|
|
625
|
+
config = safely_read_config.to_h
|
|
626
|
+
replacement_path = ConfigFiles.system_prompt_file_path(config)
|
|
572
627
|
lines = ["Active persona: #{label || "none"}"]
|
|
573
|
-
|
|
574
|
-
|
|
628
|
+
if replacement_path
|
|
629
|
+
lines << "System prompt: replacement (#{replacement_path})"
|
|
630
|
+
lines << "Global PRINCIPLES.md: ignored by replacement"
|
|
631
|
+
lines << "Workspace AGENTS.md: ignored by replacement"
|
|
632
|
+
else
|
|
633
|
+
lines << "System prompt: Kward default"
|
|
634
|
+
lines << "Global PRINCIPLES.md: #{ConfigFiles.include_config_principles?(config) ? (ConfigFiles.agents_prompt ? "present" : "absent") : "disabled"}"
|
|
635
|
+
lines << "Workspace AGENTS.md: #{ConfigFiles.workspace_agents_prompt(current_workspace_root) ? "present" : "absent"}"
|
|
636
|
+
end
|
|
575
637
|
lines << "Messages: #{conversation.messages.length}" if conversation&.respond_to?(:messages)
|
|
576
638
|
runtime_output(lines.join("\n"))
|
|
577
639
|
end
|
|
@@ -15,6 +15,9 @@ module Kward
|
|
|
15
15
|
when "stats"
|
|
16
16
|
run_busy_local_command_and_requeue { print_stats(argument) }
|
|
17
17
|
[true, nil]
|
|
18
|
+
when "sandbox"
|
|
19
|
+
run_busy_local_command_and_requeue { handle_sandbox_command(argument, interactive_workspace_root(agent)) }
|
|
20
|
+
[true, nil]
|
|
18
21
|
when "memory"
|
|
19
22
|
activity = memory_summarize_command?(argument) ? "summarizing" : "loading"
|
|
20
23
|
run_busy_local_command_and_requeue(activity: activity) { handle_memory_command(argument, agent) }
|
|
@@ -23,7 +26,11 @@ module Kward
|
|
|
23
26
|
run_busy_local_command_and_requeue { handle_hooks_command(argument) }
|
|
24
27
|
[true, nil]
|
|
25
28
|
when "skill"
|
|
26
|
-
|
|
29
|
+
if argument.to_s.strip == "capture"
|
|
30
|
+
capture_skill_from_session(session_store)
|
|
31
|
+
else
|
|
32
|
+
run_busy_local_command_and_requeue { activate_skill_command(argument, agent) }
|
|
33
|
+
end
|
|
27
34
|
[true, nil]
|
|
28
35
|
when "redraw"
|
|
29
36
|
run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
|
|
@@ -114,6 +121,48 @@ module Kward
|
|
|
114
121
|
PromptCommands.parse(command) || [nil, ""]
|
|
115
122
|
end
|
|
116
123
|
|
|
124
|
+
def handle_sandbox_command(argument, workspace_root)
|
|
125
|
+
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
126
|
+
case action
|
|
127
|
+
when nil, "", "status"
|
|
128
|
+
print_sandbox_status(workspace_root)
|
|
129
|
+
when *Sandbox::Policy::MODES
|
|
130
|
+
ConfigFiles.update_nested_config("sandbox", { "mode" => action })
|
|
131
|
+
runtime_output("Command sandbox mode set to #{action}. New sessions and tabs use the updated policy.")
|
|
132
|
+
when "network"
|
|
133
|
+
network = value.to_s
|
|
134
|
+
unless Sandbox::Policy::NETWORK_MODES.include?(network)
|
|
135
|
+
runtime_output("Usage: /sandbox network allow|deny")
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
ConfigFiles.update_nested_config("sandbox", { "network" => network })
|
|
140
|
+
runtime_output("Command sandbox child network set to #{network}. New sessions and tabs use the updated policy.")
|
|
141
|
+
else
|
|
142
|
+
runtime_output("Usage: /sandbox [status|off|read_only|workspace_write|network allow|network deny]")
|
|
143
|
+
end
|
|
144
|
+
rescue ArgumentError => error
|
|
145
|
+
runtime_output("Sandbox configuration error: #{error.message}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def print_sandbox_status(workspace_root)
|
|
149
|
+
config = ConfigFiles.read_config
|
|
150
|
+
policy = ConfigFiles.sandbox_policy(workspace_root, config)
|
|
151
|
+
runner = Sandbox::RunnerFactory.build(policy)
|
|
152
|
+
capabilities = runner.capabilities
|
|
153
|
+
lines = [
|
|
154
|
+
"Command sandbox",
|
|
155
|
+
"Mode: #{policy.mode}",
|
|
156
|
+
"Child network: #{policy.network}",
|
|
157
|
+
"Backend: #{capabilities.backend}",
|
|
158
|
+
"Filesystem enforcement: #{capabilities.filesystem_enforced? ? "active" : "inactive"}",
|
|
159
|
+
"Child-network enforcement: #{capabilities.child_network_enforced? ? "active" : "inactive"}",
|
|
160
|
+
"Scope: model-requested run_shell_command workers only"
|
|
161
|
+
]
|
|
162
|
+
lines << "Reason: #{capabilities.reason}" unless capabilities.reason.to_s.empty?
|
|
163
|
+
runtime_output(lines.join("\n"))
|
|
164
|
+
end
|
|
165
|
+
|
|
117
166
|
def open_or_resume_session(session_store, argument)
|
|
118
167
|
unless session_store
|
|
119
168
|
say_sessions_unavailable
|
|
@@ -144,6 +193,65 @@ module Kward
|
|
|
144
193
|
end
|
|
145
194
|
end
|
|
146
195
|
|
|
196
|
+
def capture_skill_from_session(session_store)
|
|
197
|
+
unless session_store
|
|
198
|
+
say_sessions_unavailable
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
unless @prompt.respond_to?(:review_document)
|
|
202
|
+
runtime_output("Skill capture is available only in the interactive terminal prompt.")
|
|
203
|
+
return
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
source_path = select_skill_capture_session(session_store)
|
|
207
|
+
return unless source_path
|
|
208
|
+
|
|
209
|
+
capture = Skills::Capture.new(session_store: session_store, client: @client)
|
|
210
|
+
draft = run_busy_local_command_and_requeue(activity: "capturing skill") { capture.generate(source_path) }
|
|
211
|
+
review_captured_skill(capture, draft)
|
|
212
|
+
rescue Skills::Capture::Error => error
|
|
213
|
+
runtime_output("Skill capture failed: #{error.message}")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def select_skill_capture_session(session_store)
|
|
217
|
+
sessions = session_store.capture_candidates
|
|
218
|
+
if sessions.empty?
|
|
219
|
+
runtime_output("No saved sessions found.")
|
|
220
|
+
return nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
labels = session_picker_labels(sessions)
|
|
224
|
+
if @prompt.respond_to?(:select)
|
|
225
|
+
choice = @prompt.select("Capture session>", labels, title: "Capture skill from session")
|
|
226
|
+
return sessions[labels.index(choice)]&.path
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
runtime_output((["Saved sessions:"] + labels.each_with_index.map { |label, index| "#{index + 1}. #{label}" }).join("\n"))
|
|
230
|
+
answer = @prompt.ask("Session number or path>").to_s.strip
|
|
231
|
+
answer.match?(/\A\d+\z/) ? sessions[answer.to_i - 1]&.path : answer
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def review_captured_skill(capture, draft)
|
|
235
|
+
overwrite = false
|
|
236
|
+
saved = nil
|
|
237
|
+
@prompt.review_document(title: "Review captured skill", content: draft.content) do |content|
|
|
238
|
+
begin
|
|
239
|
+
saved = capture.save(content, overwrite: overwrite)
|
|
240
|
+
nil
|
|
241
|
+
rescue Skills::Capture::ConflictError
|
|
242
|
+
if overwrite
|
|
243
|
+
"Could not overwrite the existing skill. Review the name and try again."
|
|
244
|
+
else
|
|
245
|
+
overwrite = true
|
|
246
|
+
"A personal skill with this name exists. Press Ctrl+S again to overwrite it."
|
|
247
|
+
end
|
|
248
|
+
rescue Skills::Capture::Error => error
|
|
249
|
+
error.message
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
runtime_output("Saved personal skill: #{capture.skill_path(saved.name)}") if saved
|
|
253
|
+
end
|
|
254
|
+
|
|
147
255
|
def activate_skill_command(name, agent)
|
|
148
256
|
skill_name = name.to_s.strip
|
|
149
257
|
if skill_name.empty?
|