kward 0.84.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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +2 -2
  5. data/doc/agent-tools.md +13 -1
  6. data/doc/api.md +17 -2
  7. data/doc/composer.md +1 -1
  8. data/doc/configuration.md +27 -4
  9. data/doc/extensibility.md +2 -1
  10. data/doc/git.md +3 -1
  11. data/doc/pan.md +6 -0
  12. data/doc/permissions.md +4 -4
  13. data/doc/plugins.md +464 -15
  14. data/doc/rpc.md +154 -16
  15. data/doc/sandboxing.md +11 -5
  16. data/doc/security.md +7 -1
  17. data/doc/session-management.md +5 -4
  18. data/doc/tabs.md +6 -2
  19. data/doc/transports.md +15 -0
  20. data/doc/usage.md +4 -1
  21. data/doc/workspace-tools.md +9 -0
  22. data/examples/plugins/space_invaders.rb +1 -1
  23. data/examples/plugins/stardate_footer.rb +2 -2
  24. data/examples/plugins/telegram/plugin.rb +1 -1
  25. data/lib/kward/agent.rb +24 -11
  26. data/lib/kward/cli/compaction.rb +9 -3
  27. data/lib/kward/cli/plugins.rb +81 -12
  28. data/lib/kward/cli/prompt_interface.rb +25 -5
  29. data/lib/kward/cli/rendering.rb +3 -0
  30. data/lib/kward/cli/runtime_helpers.rb +121 -27
  31. data/lib/kward/cli/sessions.rb +9 -5
  32. data/lib/kward/cli/settings/menus.rb +745 -0
  33. data/lib/kward/cli/settings/model.rb +327 -0
  34. data/lib/kward/cli/settings.rb +6 -1055
  35. data/lib/kward/cli/slash_commands.rb +45 -4
  36. data/lib/kward/cli/tabs.rb +167 -27
  37. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
  38. data/lib/kward/cli/worktrees.rb +65 -2
  39. data/lib/kward/cli.rb +26 -24
  40. data/lib/kward/compactor.rb +18 -7
  41. data/lib/kward/config/core.rb +389 -0
  42. data/lib/kward/config/extensions.rb +96 -0
  43. data/lib/kward/config/prompts.rb +313 -0
  44. data/lib/kward/config/settings.rb +250 -0
  45. data/lib/kward/config_files.rb +14 -1008
  46. data/lib/kward/conversation.rb +31 -2
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +2 -2
  49. data/lib/kward/model/copilot_models.rb +2 -2
  50. data/lib/kward/model/model_info.rb +20 -3
  51. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  52. data/lib/kward/model/payloads.rb +12 -3
  53. data/lib/kward/model/typesafe_client.rb +78 -0
  54. data/lib/kward/pan/server.rb +10 -7
  55. data/lib/kward/permissions/policy.rb +6 -2
  56. data/lib/kward/plugin_registry.rb +2 -659
  57. data/lib/kward/plugins/actions.rb +453 -0
  58. data/lib/kward/plugins/chat_contract.rb +121 -0
  59. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  60. data/lib/kward/plugins/host.rb +232 -0
  61. data/lib/kward/plugins/registry.rb +1190 -0
  62. data/lib/kward/plugins/resources.rb +206 -0
  63. data/lib/kward/plugins/turn_request.rb +36 -0
  64. data/lib/kward/plugins/ui.rb +219 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
  66. data/lib/kward/prompt_interface/composer_state.rb +1 -1
  67. data/lib/kward/prompt_interface/editor/controller.rb +1 -1
  68. data/lib/kward/prompt_interface/editor/runner.rb +1 -1
  69. data/lib/kward/prompt_interface/editor/state.rb +1 -1
  70. data/lib/kward/prompt_interface/layout.rb +1 -1
  71. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
  72. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  73. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  74. data/lib/kward/prompt_interface/screen.rb +9 -2
  75. data/lib/kward/prompt_interface.rb +60 -11
  76. data/lib/kward/prompts/commands.rb +2 -1
  77. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  78. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
  79. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  80. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  81. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
  82. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  83. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  84. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  85. data/lib/kward/rpc/server.rb +91 -12
  86. data/lib/kward/rpc/session_manager.rb +147 -44
  87. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  88. data/lib/kward/rpc/tool_metadata.rb +1 -1
  89. data/lib/kward/sandbox/command_runner.rb +1 -1
  90. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  91. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  92. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  93. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  94. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  95. data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
  96. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
  97. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  98. data/lib/kward/skills/trust_store.rb +1 -1
  99. data/lib/kward/tabs/driver.rb +194 -0
  100. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  101. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  102. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  103. data/lib/kward/tools/base.rb +18 -0
  104. data/lib/kward/tools/context_for_task.rb +15 -6
  105. data/lib/kward/tools/edit_file.rb +9 -6
  106. data/lib/kward/tools/git_commit.rb +13 -7
  107. data/lib/kward/tools/list_directory.rb +4 -4
  108. data/lib/kward/tools/plugin_tool.rb +41 -0
  109. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  110. data/lib/kward/tools/read_file.rb +7 -6
  111. data/lib/kward/tools/registry.rb +96 -13
  112. data/lib/kward/tools/run_shell_command.rb +10 -8
  113. data/lib/kward/tools/search/code.rb +1 -1
  114. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  115. data/lib/kward/tools/tool_call.rb +2 -1
  116. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  117. data/lib/kward/tools/workspace_targets.rb +58 -0
  118. data/lib/kward/tools/write_file.rb +9 -6
  119. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  120. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  121. data/lib/kward/transport/contracts.rb +200 -0
  122. data/lib/kward/transport/gateway.rb +79 -35
  123. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  124. data/lib/kward/transport.rb +1 -200
  125. data/lib/kward/version.rb +1 -1
  126. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  127. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  128. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  129. data/templates/default/fulldoc/html/css/kward.css +125 -0
  130. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  131. data/templates/default/fulldoc/html/setup.rb +1 -1
  132. data/templates/default/layout/html/layout.erb +16 -4
  133. metadata +67 -48
  134. data/lib/kward/tab_driver.rb +0 -90
  135. data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
  136. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  137. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  138. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  139. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  140. /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
  141. /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
  142. /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
  143. /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
  144. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  145. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  146. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  147. /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
  148. /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
  149. /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
  150. /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
  151. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  152. /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
  153. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  154. /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
  155. /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
@@ -3,7 +3,7 @@ require "set"
3
3
  require_relative "context_budget_meter"
4
4
  require_relative "image_attachments"
5
5
  require_relative "message_access"
6
- require_relative "plugin_registry"
6
+ require_relative "plugins/registry"
7
7
  require_relative "prompts"
8
8
 
9
9
  # Namespace for the Kward CLI agent runtime.
@@ -176,9 +176,38 @@ module Kward
176
176
  "toolout_#{digest}"
177
177
  end
178
178
 
179
+ # Scopes plugin instructions to one host-owned turn, including retries.
180
+ # The overlay is deliberately not persisted or folded into the base prompt.
181
+ def with_system_turn(request)
182
+ previous = @system_turn
183
+ raise ArgumentError, "A system-instruction turn is already active" if previous
184
+
185
+ @system_turn = request
186
+ yield
187
+ ensure
188
+ @system_turn = previous
189
+ end
190
+
191
+ def append_system_turn(request)
192
+ append_message(request.history_message)
193
+ end
194
+
179
195
  # @return [Array<Hash>] provider request context: current system prompt plus durable transcript
180
196
  def context_messages
181
- @system_message ? [@system_message] + @messages : @messages.dup
197
+ instruction = { role: "system", content: @system_turn.system, system_turn: true } if @system_turn
198
+ inserted = false
199
+ messages = @messages.filter_map do |message|
200
+ record = message[:plugin_system_turn] || message["plugin_system_turn"]
201
+ next message unless record
202
+ next unless @system_turn && (record[:id] || record["id"]) == @system_turn.id
203
+
204
+ inserted = true
205
+ instruction
206
+ end
207
+ # Compaction can remove the history marker; reissue the current directive
208
+ # after the compacted context rather than demoting it to the base prompt.
209
+ messages << instruction if instruction && !inserted
210
+ @system_message ? [@system_message] + messages : messages
182
211
  end
183
212
 
184
213
  # Rebuilds the system message from current config, memory, plugins, and
@@ -4,7 +4,7 @@ require "open3"
4
4
  require "shellwords"
5
5
  require "tmpdir"
6
6
  require "uri"
7
- require_relative "terminal_image_support"
7
+ require_relative "terminal/image_support"
8
8
 
9
9
  # Namespace for the Kward CLI agent runtime.
10
10
  module Kward
@@ -8,7 +8,7 @@ require_relative "../auth/openai_oauth"
8
8
  require_relative "../cancellation"
9
9
  require_relative "../config_files"
10
10
  require_relative "../http"
11
- require_relative "../openrouter_model_cache"
11
+ require_relative "openrouter_model_cache"
12
12
  require_relative "context_overflow"
13
13
  require_relative "azure_openai_config"
14
14
  require_relative "copilot_models"
@@ -712,7 +712,7 @@ module Kward
712
712
  end
713
713
 
714
714
  def copilot_responses_model?(model)
715
- model.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
715
+ model.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/)
716
716
  end
717
717
 
718
718
  def model_entry(provider, id, current: false, metadata: nil)
@@ -14,7 +14,7 @@ module Kward
14
14
 
15
15
  def supported?(model)
16
16
  text = model.to_s
17
- text.match?(/\Agpt-5(?:\.|-|\z)/) || text.match?(/\A(?:gemini-|gpt-4\.1|oswe-)/)
17
+ text.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/) || text.match?(/\A(?:gemini-|gpt-4\.1|oswe-)/)
18
18
  end
19
19
 
20
20
  def supported_choices(choices)
@@ -24,7 +24,7 @@ module Kward
24
24
  def resolved_chat_model(configured_model, choices)
25
25
  return configured_model if choices.empty? || choices.include?(configured_model)
26
26
 
27
- choices.find { |model| supported?(model) } || raise("No Copilot models supported by Kward are available for this account. Kward currently supports Copilot GPT-5 Responses and Gemini/GPT-4.1 chat models.")
27
+ choices.find { |model| supported?(model) } || raise("No Copilot models supported by Kward are available for this account. Kward currently supports Copilot GPT-5/GPT-6 Responses and Gemini/GPT-4.1 chat models.")
28
28
  end
29
29
 
30
30
  def catalog_entries(body)
@@ -9,7 +9,7 @@ module Kward
9
9
  DEFAULT_COPILOT_MODEL = "gpt-5-mini"
10
10
  DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5"
11
11
  DEFAULT_REASONING_EFFORT = "medium"
12
- OPENAI_MODEL_CHOICES = %w[gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
12
+ OPENAI_MODEL_CHOICES = %w[gpt-6-astra gpt-6.1-sol gpt-6-luna gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
13
13
  ANTHROPIC_MODEL_CHOICES = %w[
14
14
  claude-fable-5
15
15
  claude-opus-5
@@ -24,6 +24,7 @@ module Kward
24
24
  ].freeze
25
25
  COPILOT_MODEL_CHOICES = %w[
26
26
  gpt-5-mini
27
+ gpt-6-astra
27
28
  gpt-5.3-codex
28
29
  gpt-5.4
29
30
  gpt-5.4-mini
@@ -54,6 +55,14 @@ module Kward
54
55
  *OPENAI_REASONING_EFFORT_CHOICES,
55
56
  ["max", "Max"]
56
57
  ].freeze
58
+ GPT_6_REASONING_EFFORT_CHOICES = [
59
+ *REASONING_EFFORT_CHOICES,
60
+ ["max", "Max"]
61
+ ].freeze
62
+ GPT_6_LUNA_REASONING_EFFORT_CHOICES = [
63
+ *OPENAI_REASONING_EFFORT_CHOICES,
64
+ ["max", "Max"]
65
+ ].freeze
57
66
  ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES = [
58
67
  ["low", "Low"],
59
68
  ["medium", "Medium"],
@@ -78,6 +87,7 @@ module Kward
78
87
  ].freeze
79
88
 
80
89
  CODEX_CONTEXT_WINDOWS = [
90
+ [/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/, 1_050_000],
81
91
  [/\Agpt-5\.6/, 1_050_000],
82
92
  [/\Agpt-5\.5/, 400_000],
83
93
  [/\Agpt-5\.4-mini/, 400_000],
@@ -96,6 +106,7 @@ module Kward
96
106
  [/\Agpt-3\.5-turbo/, 16_385]
97
107
  ].freeze
98
108
  OPENAI_CONTEXT_WINDOWS = [
109
+ [/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/, 1_050_000],
99
110
  [/\Agpt-5\.6/, 1_050_000],
100
111
  [/\Agpt-5\.5/, 1_050_000],
101
112
  [/\Agpt-5\.4-mini/, 400_000],
@@ -423,10 +434,12 @@ module Kward
423
434
  case provider
424
435
  when "Codex", "OpenRouter"
425
436
  true
437
+ when "OpenAI"
438
+ id.to_s.match?(/\Agpt-6(?:\.1)?-(?:astra|sol|luna)(?:\z|-)/)
426
439
  when "Anthropic"
427
440
  !reasoning_effort_choices(provider, id).empty?
428
441
  when "Copilot"
429
- id.to_s.match?(/\Agpt-5(?:\.|-|\z)/)
442
+ id.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/)
430
443
  else
431
444
  false
432
445
  end
@@ -436,10 +449,12 @@ module Kward
436
449
  case provider
437
450
  when "Codex", "OpenRouter"
438
451
  openai_reasoning_effort_choices(id)
452
+ when "OpenAI"
453
+ reasoning_supported?(provider, id) ? openai_reasoning_effort_choices(id) : []
439
454
  when "Anthropic"
440
455
  anthropic_reasoning_effort_choices(id)
441
456
  when "Copilot"
442
- id.to_s.match?(/\Agpt-5(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : []
457
+ id.to_s.match?(/\Agpt-(?:5|6)(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : []
443
458
  else
444
459
  []
445
460
  end
@@ -447,6 +462,8 @@ module Kward
447
462
 
448
463
  def openai_reasoning_effort_choices(id)
449
464
  text = id.to_s.delete_prefix("openai/")
465
+ return GPT_6_LUNA_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-6-luna(?:\z|-)/)
466
+ return GPT_6_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-6\.1-sol(?:\z|-)|\Agpt-6-astra(?:\z|-)/)
450
467
  return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/)
451
468
  return OPENAI_MAX_REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.6(?:\z|-)/)
452
469
 
@@ -3,9 +3,9 @@ require "json"
3
3
  require "net/http"
4
4
  require "time"
5
5
  require "uri"
6
- require_relative "config_files"
7
- require_relative "http"
8
- require_relative "private_file"
6
+ require_relative "../config_files"
7
+ require_relative "../http"
8
+ require_relative "../private_file"
9
9
 
10
10
  # Namespace for the Kward CLI agent runtime.
11
11
  module Kward
@@ -95,6 +95,8 @@ module Kward
95
95
 
96
96
  def anthropic_payload(messages, tools, max_tokens: nil, model: nil, reasoning: nil)
97
97
  parts = build_context_parts("Anthropic", messages, tools, model: model)
98
+ raise ArgumentError, "Anthropic requires conversation messages; a system-only turn is unavailable in an empty session" if parts[:messages].empty?
99
+
98
100
  system = [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }]
99
101
  system << { type: "text", text: parts[:system] } unless parts[:system].to_s.empty?
100
102
  payload = {
@@ -122,6 +124,8 @@ module Kward
122
124
  contents: messages.filter_map { |message| gemini_message(message) },
123
125
  tools: tools.empty? ? nil : [{ functionDeclarations: tools.map { |tool| gemini_tool_schema(tool) } }]
124
126
  }.compact
127
+ raise ArgumentError, "Gemini requires conversation contents; a system-only turn is unavailable in an empty session" if payload[:contents].empty?
128
+
125
129
  payload[:systemInstruction] = { parts: system_parts } unless system_parts.empty?
126
130
  payload[:generationConfig] = { maxOutputTokens: max_tokens.to_i } if max_tokens.to_i.positive?
127
131
  payload
@@ -219,7 +223,8 @@ module Kward
219
223
  tools: tools.map { |tool| codex_tool_schema(tool) }
220
224
  }
221
225
  elsif ["Codex", "OpenAI"].include?(provider)
222
- instructions, input = codex_messages(messages)
226
+ # The Codex subscription endpoint rejects inline system messages.
227
+ instructions, input = codex_messages(messages, system_turn_role: provider == "Codex" ? "developer" : "system")
223
228
  {
224
229
  provider: provider,
225
230
  model: model_for(provider, override_model: model),
@@ -335,7 +340,7 @@ module Kward
335
340
  }
336
341
  end
337
342
 
338
- def codex_messages(messages)
343
+ def codex_messages(messages, system_turn_role: "system")
339
344
  instructions = []
340
345
  input = []
341
346
 
@@ -344,7 +349,11 @@ module Kward
344
349
  content = MessageAccess.content(message) || ""
345
350
  case role.to_s
346
351
  when "system"
347
- instructions << plain_content(content).to_s
352
+ if MessageAccess.value(message, :system_turn)
353
+ input << codex_message(system_turn_role, plain_content(content).to_s)
354
+ else
355
+ instructions << plain_content(content).to_s
356
+ end
348
357
  when "tool", "toolResult"
349
358
  input << {
350
359
  type: "function_call_output",
@@ -0,0 +1,78 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+ require_relative "../cancellation"
5
+ require_relative "../http"
6
+
7
+ module Kward
8
+ # HTTP client for TypeSafe's structured System One evaluation endpoint.
9
+ class TypeSafeClient
10
+ DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
11
+ DEFAULT_MODEL = "jev-latest"
12
+ MAX_STATE_BYTES = 100_000
13
+ MAX_QUESTIONS = 100
14
+ HTTP_TIMEOUT_SECONDS = 30
15
+
16
+ class RequestError < StandardError
17
+ attr_reader :status
18
+
19
+ def initialize(status:, message:)
20
+ @status = status.to_i
21
+ super(message)
22
+ end
23
+ end
24
+
25
+ def initialize(api_key: ENV["TYPESAFE_API_KEY"], endpoint: DEFAULT_ENDPOINT, http: Net::HTTP)
26
+ @api_key = api_key.to_s.strip
27
+ @endpoint = URI(endpoint)
28
+ @http = http
29
+ end
30
+
31
+ def available?
32
+ !@api_key.empty?
33
+ end
34
+
35
+ def evaluate(state:, questions:, model: DEFAULT_MODEL, cancellation: nil)
36
+ raise ArgumentError, "TypeSafe API key is not configured" unless available?
37
+
38
+ cancellation&.raise_if_cancelled!
39
+ request = Net::HTTP::Post.new(@endpoint)
40
+ request["Authorization"] = "Bearer #{@api_key}"
41
+ request["Content-Type"] = "application/json"
42
+ request["Accept"] = "application/json"
43
+ Http.apply_user_agent(request)
44
+ request.body = JSON.dump({ state: state, model: model, questions: questions })
45
+
46
+ response_body = nil
47
+ @http.start(@endpoint.hostname, @endpoint.port, use_ssl: @endpoint.scheme == "https", read_timeout: HTTP_TIMEOUT_SECONDS) do |http|
48
+ cancellation&.on_cancel { close_http(http) }
49
+ cancellation&.raise_if_cancelled!
50
+ response = http.request(request)
51
+ response_body = response.body.to_s
52
+ unless response.is_a?(Net::HTTPSuccess)
53
+ raise RequestError.new(status: response.code, message: error_message(response.code, response_body))
54
+ end
55
+ end
56
+ cancellation&.raise_if_cancelled!
57
+ JSON.parse(response_body)
58
+ rescue JSON::ParserError
59
+ raise RequestError.new(status: 200, message: "TypeSafe returned invalid JSON")
60
+ end
61
+
62
+ private
63
+
64
+ def error_message(status, body)
65
+ detail = JSON.parse(body).fetch("error", nil) rescue nil
66
+ detail = detail["message"] if detail.is_a?(Hash)
67
+ detail = detail.to_s unless detail.nil?
68
+ detail = "request failed" if detail.nil? || detail.empty?
69
+ "TypeSafe request failed (#{status}): #{detail[0, 500]}"
70
+ end
71
+
72
+ def close_http(http)
73
+ http.finish if http&.started?
74
+ rescue IOError
75
+ nil
76
+ end
77
+ end
78
+ end
@@ -10,16 +10,16 @@ require_relative "../agent"
10
10
  require_relative "../config_files"
11
11
  require_relative "../events"
12
12
  require_relative "../model/retry_message"
13
- require_relative "../plugin_registry"
13
+ require_relative "../plugins/registry"
14
14
  require_relative "../prompts/commands"
15
15
  require_relative "../rpc/transcript_normalizer"
16
- require_relative "../session_store"
16
+ require_relative "../sessions/store"
17
17
  require_relative "../skills/capture"
18
- require_relative "../session_trash"
18
+ require_relative "../sessions/trash"
19
19
  require_relative "../tools/tool_call"
20
20
  require_relative "../tools/registry"
21
21
  require_relative "../version"
22
- require_relative "../workspace_factory"
22
+ require_relative "../workspace/factory"
23
23
 
24
24
  # Namespace for the Kward CLI agent runtime.
25
25
  module Kward
@@ -95,6 +95,8 @@ module Kward
95
95
  @server&.close unless @server&.closed?
96
96
  rescue IOError
97
97
  nil
98
+ ensure
99
+ @plugin_registry&.shutdown!
98
100
  end
99
101
 
100
102
  def enqueue_prompt(prompt)
@@ -130,7 +132,7 @@ module Kward
130
132
  hook_manager = lifecycle_hook_manager
131
133
  Agent.new(
132
134
  client: @client,
133
- tool_registry: ToolRegistry.new(workspace: @workspace, ask_user_question_enabled: false, hook_manager: hook_manager, hook_context: hook_context),
135
+ tool_registry: ToolRegistry.new(workspace: @workspace, ask_user_question_enabled: false, plugin_tools: plugin_registry.tools, hook_manager: hook_manager, hook_context: hook_context),
134
136
  conversation: @conversation,
135
137
  hook_manager: hook_manager,
136
138
  hook_context: hook_context
@@ -177,7 +179,7 @@ module Kward
177
179
  end
178
180
 
179
181
  def plugin_registry
180
- @plugin_registry ||= PluginRegistry.load(reserved_commands: PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES)
182
+ @plugin_registry ||= PluginRegistry.load(reserved_commands: PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES).tap(&:start!)
181
183
  end
182
184
 
183
185
  def start_worker
@@ -241,7 +243,8 @@ module Kward
241
243
  when ["GET", "/kward-logo.png"]
242
244
  write_response(socket, 200, { "Content-Type" => "image/png", "Cache-Control" => "public, max-age=86400" }, File.binread(File.join(__dir__, "kward_logo.png")))
243
245
  when ["GET", "/transcript"]
244
- write_json(socket, 200, transcript: transcript_items, session: active_session_payload, workspace: @workspace.root.to_s)
246
+ write_json(socket, 200, transcript: transcript_items, session: active_session_payload, workspace: @workspace.root.to_s,
247
+ capabilities: { pluginCommandTurns: { supported: false, reason: "Pan has no plugin slash-command dispatcher; use the TUI or RPC turns/start." } })
245
248
  when ["GET", "/sessions"]
246
249
  write_json(socket, 200, sessions: session_payloads, activeSessionId: @session.id)
247
250
  when ["GET", "/skill-capture/sessions"]
@@ -160,11 +160,15 @@ module Kward
160
160
  end
161
161
 
162
162
  def network?(request)
163
- NETWORK_TOOLS.include?(request.fetch("tool")) || request["source"] == "mcp"
163
+ NETWORK_TOOLS.include?(request.fetch("tool"))
164
+ end
165
+
166
+ def extension_tool?(request)
167
+ %w[mcp plugin].include?(request["source"])
164
168
  end
165
169
 
166
170
  def risky?(request)
167
- mutating?(request) || request.fetch("tool") == "run_shell_command" || network?(request)
171
+ mutating?(request) || request.fetch("tool") == "run_shell_command" || network?(request) || extension_tool?(request)
168
172
  end
169
173
 
170
174
  def outside_write_scopes?(request)