kward 0.67.0 → 0.68.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +5 -5
  5. data/doc/authentication.md +24 -1
  6. data/doc/configuration.md +9 -2
  7. data/doc/extensibility.md +1 -1
  8. data/doc/getting-started.md +4 -6
  9. data/doc/plugins.md +0 -2
  10. data/doc/releasing.md +7 -8
  11. data/doc/rpc.md +6 -6
  12. data/doc/usage.md +5 -2
  13. data/doc/web-search.md +2 -2
  14. data/kward.gemspec +4 -0
  15. data/lib/kward/agent.rb +29 -2
  16. data/lib/kward/ansi.rb +3 -0
  17. data/lib/kward/auth/anthropic_oauth.rb +291 -0
  18. data/lib/kward/auth/file.rb +2 -0
  19. data/lib/kward/auth/github_oauth.rb +3 -0
  20. data/lib/kward/auth/openai_oauth.rb +4 -0
  21. data/lib/kward/auth/openrouter_api_key.rb +2 -0
  22. data/lib/kward/cancellation.rb +3 -0
  23. data/lib/kward/cli/auth_commands.rb +82 -0
  24. data/lib/kward/cli/commands.rb +222 -0
  25. data/lib/kward/cli/compaction.rb +25 -0
  26. data/lib/kward/cli/doctor.rb +121 -0
  27. data/lib/kward/cli/interactive_turn.rb +225 -0
  28. data/lib/kward/cli/memory_commands.rb +133 -0
  29. data/lib/kward/cli/plugins.rb +112 -0
  30. data/lib/kward/cli/prompt_interface.rb +132 -0
  31. data/lib/kward/cli/rendering.rb +389 -0
  32. data/lib/kward/cli/runtime_helpers.rb +159 -0
  33. data/lib/kward/cli/sessions.rb +376 -0
  34. data/lib/kward/cli/settings.rb +663 -0
  35. data/lib/kward/cli/slash_commands.rb +112 -0
  36. data/lib/kward/cli/stats.rb +64 -0
  37. data/lib/kward/cli/tool_summaries.rb +153 -0
  38. data/lib/kward/cli.rb +38 -2790
  39. data/lib/kward/cli_transcript_formatter.rb +4 -7
  40. data/lib/kward/clipboard.rb +1 -0
  41. data/lib/kward/compaction/file_operation_tracker.rb +3 -0
  42. data/lib/kward/compactor.rb +29 -7
  43. data/lib/kward/config_files.rb +33 -24
  44. data/lib/kward/conversation.rb +70 -5
  45. data/lib/kward/events.rb +2 -0
  46. data/lib/kward/export_path.rb +2 -0
  47. data/lib/kward/image_attachments.rb +2 -0
  48. data/lib/kward/markdown_transcript.rb +2 -0
  49. data/lib/kward/memory/manager.rb +13 -0
  50. data/lib/kward/message_access.rb +23 -2
  51. data/lib/kward/message_text.rb +45 -0
  52. data/lib/kward/model/chat_invocation.rb +2 -0
  53. data/lib/kward/model/client.rb +295 -77
  54. data/lib/kward/model/context_overflow.rb +2 -0
  55. data/lib/kward/model/context_usage.rb +3 -0
  56. data/lib/kward/model/model_info.rb +143 -4
  57. data/lib/kward/model/payloads.rb +166 -13
  58. data/lib/kward/model/retry_message.rb +2 -0
  59. data/lib/kward/model/stream_parser.rb +129 -0
  60. data/lib/kward/pan/server.rb +3 -1
  61. data/lib/kward/plugin_registry.rb +12 -0
  62. data/lib/kward/private_file.rb +2 -0
  63. data/lib/kward/prompt_interface/banner.rb +3 -0
  64. data/lib/kward/prompt_interface/composer_controller.rb +262 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
  66. data/lib/kward/prompt_interface/composer_state.rb +221 -0
  67. data/lib/kward/prompt_interface/key_handler.rb +365 -0
  68. data/lib/kward/prompt_interface/layout.rb +31 -0
  69. data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
  70. data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
  71. data/lib/kward/prompt_interface/question_prompt.rb +328 -0
  72. data/lib/kward/prompt_interface/runtime_state.rb +59 -0
  73. data/lib/kward/prompt_interface/screen.rb +186 -0
  74. data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
  75. data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
  76. data/lib/kward/prompt_interface/stream_state.rb +65 -0
  77. data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
  78. data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
  79. data/lib/kward/prompt_interface.rb +69 -1832
  80. data/lib/kward/prompts/commands.rb +2 -0
  81. data/lib/kward/prompts/templates.rb +3 -0
  82. data/lib/kward/prompts.rb +2 -0
  83. data/lib/kward/question_contract.rb +66 -0
  84. data/lib/kward/resources/avatar_kward_logo.rb +2 -0
  85. data/lib/kward/resources/pixel_logo.rb +2 -0
  86. data/lib/kward/rpc/attachment_normalizer.rb +60 -0
  87. data/lib/kward/rpc/auth_manager.rb +65 -11
  88. data/lib/kward/rpc/config_manager.rb +11 -0
  89. data/lib/kward/rpc/prompt_bridge.rb +5 -26
  90. data/lib/kward/rpc/redactor.rb +3 -0
  91. data/lib/kward/rpc/runtime_payloads.rb +4 -1
  92. data/lib/kward/rpc/server.rb +37 -10
  93. data/lib/kward/rpc/session_manager.rb +123 -347
  94. data/lib/kward/rpc/session_metrics.rb +68 -0
  95. data/lib/kward/rpc/session_tree.rb +48 -0
  96. data/lib/kward/rpc/session_tree_rows.rb +208 -0
  97. data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
  98. data/lib/kward/rpc/tool_metadata.rb +3 -0
  99. data/lib/kward/rpc/transcript_normalizer.rb +3 -0
  100. data/lib/kward/rpc/transport.rb +3 -0
  101. data/lib/kward/session_diff.rb +2 -0
  102. data/lib/kward/session_store.rb +125 -31
  103. data/lib/kward/session_trash.rb +1 -0
  104. data/lib/kward/session_tree_renderer.rb +8 -41
  105. data/lib/kward/session_tree_tool_display.rb +56 -0
  106. data/lib/kward/skills/registry.rb +3 -0
  107. data/lib/kward/starter_pack_installer.rb +1 -0
  108. data/lib/kward/steering.rb +2 -0
  109. data/lib/kward/telemetry/logger.rb +3 -0
  110. data/lib/kward/telemetry/stats.rb +3 -0
  111. data/lib/kward/tools/ask_user_question.rb +20 -32
  112. data/lib/kward/tools/base.rb +8 -0
  113. data/lib/kward/tools/code_search.rb +5 -0
  114. data/lib/kward/tools/edit_file.rb +5 -0
  115. data/lib/kward/tools/list_directory.rb +5 -0
  116. data/lib/kward/tools/read_file.rb +5 -0
  117. data/lib/kward/tools/read_skill.rb +5 -0
  118. data/lib/kward/tools/registry.rb +33 -2
  119. data/lib/kward/tools/run_shell_command.rb +5 -0
  120. data/lib/kward/tools/search/code.rb +7 -0
  121. data/lib/kward/tools/search/web.rb +17 -14
  122. data/lib/kward/tools/tool_call.rb +25 -5
  123. data/lib/kward/tools/web_search.rb +7 -1
  124. data/lib/kward/tools/write_file.rb +5 -0
  125. data/lib/kward/transcript_export.rb +2 -0
  126. data/lib/kward/version.rb +2 -1
  127. data/lib/kward/workspace.rb +45 -5
  128. metadata +43 -1
@@ -1,6 +1,8 @@
1
1
  require_relative "../config_files"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Prompt-template and slash-command parsing helpers.
4
6
  module PromptCommands
5
7
  BUILTIN_COMMANDS = [
6
8
  { name: "exit", description: "Exit the interactive session.", argument_hint: "" },
@@ -1,5 +1,8 @@
1
+ # Namespace for the Kward CLI agent runtime.
1
2
  module Kward
3
+ # Prompt template discovery from configured markdown files.
2
4
  module Prompts
5
+ # Parsed prompt template loaded from disk.
3
6
  class Templates
4
7
  def initialize(config_dir:, template_class:, markdown_parser:)
5
8
  @config_dir = config_dir
data/lib/kward/prompts.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require_relative "config_files"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # System prompt assembly from config, workspace instructions, memory, and plugins.
4
6
  module Prompts
5
7
  module_function
6
8
 
@@ -0,0 +1,66 @@
1
+ require_relative "message_access"
2
+
3
+ # Namespace for the Kward CLI agent runtime.
4
+ module Kward
5
+ # Validates and normalizes structured clarification questions shared by CLI
6
+ # tools and RPC prompt bridging.
7
+ module QuestionContract
8
+ MIN_QUESTIONS = 1
9
+ MAX_QUESTIONS = 4
10
+ MIN_OPTIONS = 2
11
+ MAX_OPTIONS = 4
12
+
13
+ module_function
14
+
15
+ def normalize_questions(questions)
16
+ raise ArgumentError, "questions must be an array" unless questions.is_a?(Array)
17
+ unless questions.length.between?(MIN_QUESTIONS, MAX_QUESTIONS)
18
+ raise ArgumentError, "ui/question requires 1-4 questions"
19
+ end
20
+
21
+ questions.map.with_index(1) { |question, index| normalize_question(question, index) }
22
+ end
23
+
24
+ def normalize_question(question, index)
25
+ raise ArgumentError, "question #{index} must be an object" unless question.is_a?(Hash)
26
+ raise ArgumentError, "question #{index} multiSelect is unsupported" if has_key?(question, :multiSelect)
27
+
28
+ text = value(question, :question).to_s.strip
29
+ header = value(question, :header).to_s.strip
30
+ raise ArgumentError, "question #{index} requires question and header" if text.empty? || header.empty?
31
+
32
+ options = value(question, :options)
33
+ raise ArgumentError, "question #{index} options must be an array" unless options.is_a?(Array)
34
+ unless options.length.between?(MIN_OPTIONS, MAX_OPTIONS)
35
+ raise ArgumentError, "question #{index} requires 2-4 options"
36
+ end
37
+
38
+ {
39
+ question: text,
40
+ header: header,
41
+ options: options.map.with_index(1) { |option, option_index| normalize_option(option, index, option_index) }
42
+ }
43
+ end
44
+
45
+ def normalize_option(option, question_index, option_index)
46
+ raise ArgumentError, "question #{question_index} option #{option_index} must be an object" unless option.is_a?(Hash)
47
+ raise ArgumentError, "question #{question_index} preview is unsupported" if has_key?(option, :preview)
48
+
49
+ label = value(option, :label).to_s.strip
50
+ description = value(option, :description).to_s.strip
51
+ if label.empty? || description.empty?
52
+ raise ArgumentError, "question #{question_index} option #{option_index} requires label and description"
53
+ end
54
+
55
+ { label: label, description: description }
56
+ end
57
+
58
+ def value(object, key)
59
+ MessageAccess.value(object, key)
60
+ end
61
+
62
+ def has_key?(object, key)
63
+ object.key?(key) || object.key?(key.to_s)
64
+ end
65
+ end
66
+ end
@@ -1,7 +1,9 @@
1
1
  # Generated from avatar_kward_32x32.png as RGB terminal cells.
2
2
  # The interactive banner uses this data instead of decoding a PNG at runtime.
3
3
  module Kward
4
+ # Static avatar logo data used by generated resources.
4
5
  module Resources
6
+ # Static avatar logo data used by generated resources.
5
7
  module AvatarKwardLogo
6
8
  PIXELS = [
7
9
  [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
@@ -1,6 +1,8 @@
1
1
  require "zlib"
2
2
 
3
+ # Namespace for the Kward CLI agent runtime.
3
4
  module Kward
5
+ # Pixel-art logo data and rendering helpers.
4
6
  module PixelLogo
5
7
  PNG_SIGNATURE = "\x89PNG\r\n\x1a\n".b.freeze
6
8
  TRANSPARENT_ALPHA = 128
@@ -0,0 +1,60 @@
1
+ require "base64"
2
+ require_relative "../tools/tool_call"
3
+
4
+ # Namespace for the Kward CLI agent runtime.
5
+ module Kward
6
+ # JSON-RPC backend namespace used by UI clients.
7
+ module RPC
8
+ # Validates and normalizes RPC image attachments.
9
+ class AttachmentNormalizer
10
+ IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"].freeze
11
+ MAX_BYTES = 10 * 1024 * 1024
12
+
13
+ def initialize(max_bytes: MAX_BYTES, mime_types: IMAGE_MIME_TYPES)
14
+ @max_bytes = max_bytes
15
+ @mime_types = mime_types
16
+ end
17
+
18
+ def normalize(attachments)
19
+ return [] if attachments.nil?
20
+ raise ArgumentError, "attachments must be an array" unless attachments.is_a?(Array)
21
+
22
+ attachments.map { |attachment| normalize_attachment(attachment) }
23
+ end
24
+
25
+ private
26
+
27
+ def normalize_attachment(attachment)
28
+ raise ArgumentError, "attachment must be an object" unless attachment.is_a?(Hash)
29
+
30
+ type = ToolCall.value(attachment, :type).to_s
31
+ raise ArgumentError, "Unsupported attachment type: #{type.empty? ? "unknown" : type}" unless type == "image"
32
+
33
+ mime_type = normalize_mime_type(ToolCall.value(attachment, :mimeType) || ToolCall.value(attachment, :mime_type) || ToolCall.value(attachment, :media_type))
34
+ raise ArgumentError, "Unsupported image MIME type: #{mime_type.empty? ? "unknown" : mime_type}" unless @mime_types.include?(mime_type)
35
+
36
+ data = ToolCall.value(attachment, :data).to_s
37
+ raise ArgumentError, "Image attachment data must be valid base64" if data.empty?
38
+ raise ArgumentError, "Image attachment data must be raw base64" if data.start_with?("data:")
39
+ declared_size = ToolCall.value(attachment, :sizeBytes) || ToolCall.value(attachment, :size_bytes)
40
+ raise ArgumentError, "Image attachment is too large" if declared_size && declared_size.to_i > @max_bytes
41
+
42
+ decoded_size = Base64.strict_decode64(data).bytesize
43
+ raise ArgumentError, "Image attachment is too large" if decoded_size > @max_bytes
44
+
45
+ result = { type: "image", data: data, mimeType: mime_type }
46
+ name = ToolCall.value(attachment, :name)
47
+ result[:alt] = name.to_s unless name.to_s.empty?
48
+ result
49
+ rescue ArgumentError => e
50
+ raise e if e.message.start_with?("Unsupported", "Image attachment", "attachment")
51
+
52
+ raise ArgumentError, "Image attachment data must be valid base64"
53
+ end
54
+
55
+ def normalize_mime_type(mime_type)
56
+ mime_type.to_s.downcase
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,19 +1,24 @@
1
1
  require "securerandom"
2
2
  require "thread"
3
+ require_relative "../auth/anthropic_oauth"
3
4
  require_relative "../auth/github_oauth"
4
5
  require_relative "../auth/openai_oauth"
5
6
  require_relative "../model/client"
6
7
  require_relative "config_manager"
7
8
 
9
+ # Namespace for the Kward CLI agent runtime.
8
10
  module Kward
11
+ # JSON-RPC backend namespace used by UI clients.
9
12
  module RPC
13
+ # RPC authentication manager for provider status, login, and logout requests.
10
14
  class AuthManager
11
- Login = Struct.new(:id, :oauth, :pkce, :state, :server, :redirect_uri, :status, :error, :thread, keyword_init: true)
15
+ Login = Struct.new(:id, :provider_id, :oauth, :pkce, :state, :server, :redirect_uri, :status, :error, :thread, keyword_init: true)
12
16
 
13
- def initialize(server:, oauth_factory: -> { OpenAIOAuth.new }, github_oauth_factory: -> { GithubOAuth.new }, config_manager: ConfigManager.new)
17
+ def initialize(server:, oauth_factory: -> { OpenAIOAuth.new }, github_oauth_factory: -> { GithubOAuth.new }, anthropic_oauth_factory: -> { AnthropicOAuth.new }, config_manager: ConfigManager.new)
14
18
  @server = server
15
19
  @oauth_factory = oauth_factory
16
20
  @github_oauth_factory = github_oauth_factory
21
+ @anthropic_oauth_factory = anthropic_oauth_factory
17
22
  @config_manager = config_manager
18
23
  @logins = {}
19
24
  @mutex = Mutex.new
@@ -27,6 +32,7 @@ module Kward
27
32
  openaiAccountId: oauth.respond_to?(:account_id) ? oauth.account_id : nil,
28
33
  openrouterApiKey: !ENV["OPENROUTER_API_KEY"].to_s.empty? || !config["openrouter_api_key"].to_s.empty?,
29
34
  openaiAccessToken: !ENV["OPENAI_ACCESS_TOKEN"].to_s.empty?,
35
+ anthropicOAuth: @anthropic_oauth_factory.call.logged_in?,
30
36
  githubOAuth: @github_oauth_factory.call.logged_in?
31
37
  }
32
38
  rescue StandardError => e
@@ -34,7 +40,7 @@ module Kward
34
40
  end
35
41
 
36
42
  def providers
37
- { providers: [openai_provider, openrouter_provider, github_provider] }
43
+ { providers: [openai_provider, anthropic_provider, openrouter_provider, github_provider] }
38
44
  end
39
45
 
40
46
  def login_with_api_key(provider_id:, api_key:)
@@ -49,6 +55,9 @@ module Kward
49
55
  when "openai"
50
56
  logout_openai
51
57
  { providerId: provider_id, message: "Logged out of OpenAI." }
58
+ when "anthropic"
59
+ logout_anthropic
60
+ { providerId: provider_id, message: "Logged out of Anthropic." }
52
61
  when "openrouter"
53
62
  @config_manager.delete_key("openrouter_api_key")
54
63
  { providerId: provider_id, message: "Logged out of OpenRouter." }
@@ -61,7 +70,9 @@ module Kward
61
70
  provider_id = provider_id.to_s
62
71
  case provider_id
63
72
  when "openai"
64
- start_openai_login(timeout_seconds: timeout_seconds)
73
+ start_oauth_login(provider_id: "openai", oauth: @oauth_factory.call, timeout_seconds: timeout_seconds)
74
+ when "anthropic"
75
+ start_oauth_login(provider_id: "anthropic", oauth: @anthropic_oauth_factory.call, timeout_seconds: timeout_seconds)
65
76
  when "github"
66
77
  raise "GitHub OAuth is supported in the CLI with `ruby lib/main.rb login github`, but RPC browser login is not implemented yet."
67
78
  else
@@ -70,7 +81,10 @@ module Kward
70
81
  end
71
82
 
72
83
  def start_openai_login(timeout_seconds: 120)
73
- oauth = @oauth_factory.call
84
+ start_oauth_login(provider_id: "openai", oauth: @oauth_factory.call, timeout_seconds: timeout_seconds)
85
+ end
86
+
87
+ def start_oauth_login(provider_id:, oauth:, timeout_seconds: 120)
74
88
  flow = oauth.start_login_flow
75
89
  pkce = flow.fetch(:pkce)
76
90
  state = flow.fetch(:state)
@@ -79,6 +93,7 @@ module Kward
79
93
  url = flow.fetch(:authorization_url)
80
94
  login = Login.new(
81
95
  id: SecureRandom.uuid,
96
+ provider_id: provider_id,
82
97
  oauth: oauth,
83
98
  pkce: pkce,
84
99
  state: state,
@@ -88,7 +103,7 @@ module Kward
88
103
  )
89
104
  @mutex.synchronize { @logins[login.id] = login }
90
105
  login.thread = Thread.new { wait_for_callback(login, timeout_seconds: timeout_seconds.to_i <= 0 ? 120 : timeout_seconds.to_i) }
91
- { providerId: "openai", loginId: login.id, authorizationUrl: url, redirectUri: redirect_uri, status: login.status }
106
+ { providerId: provider_id, loginId: login.id, authorizationUrl: url, redirectUri: redirect_uri, status: login.status }
92
107
  end
93
108
 
94
109
  def submit_openai_code(login_id:, code:)
@@ -146,6 +161,34 @@ module Kward
146
161
  }.compact
147
162
  end
148
163
 
164
+ def anthropic_provider
165
+ oauth = @anthropic_oauth_factory.call
166
+ stored_configured = oauth.logged_in?
167
+ provider = {
168
+ id: "anthropic",
169
+ name: "Anthropic",
170
+ authType: "oauth",
171
+ configured: stored_configured,
172
+ storedCredentialType: "oauth",
173
+ canLogout: stored_configured,
174
+ usesCallbackServer: true
175
+ }
176
+ provider[:source] = "stored" if provider[:configured]
177
+ provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
178
+ provider
179
+ rescue StandardError
180
+ {
181
+ id: "anthropic",
182
+ name: "Anthropic",
183
+ authType: "oauth",
184
+ configured: false,
185
+ label: "Not signed in",
186
+ storedCredentialType: "oauth",
187
+ canLogout: false,
188
+ usesCallbackServer: true
189
+ }
190
+ end
191
+
149
192
  def openrouter_provider
150
193
  config = stored_config
151
194
  env_configured = !ENV["OPENROUTER_API_KEY"].to_s.empty?
@@ -200,6 +243,7 @@ module Kward
200
243
  case provider_id
201
244
  when "openrouter" then "OpenRouter"
202
245
  when "openai" then "OpenAI"
246
+ when "anthropic" then "Anthropic"
203
247
  when "github" then "GitHub"
204
248
  else provider_id
205
249
  end
@@ -211,6 +255,12 @@ module Kward
211
255
  File.delete(path) if path && File.exist?(path)
212
256
  end
213
257
 
258
+ def logout_anthropic
259
+ oauth = @anthropic_oauth_factory.call
260
+ path = oauth.auth_path if oauth.respond_to?(:auth_path)
261
+ File.delete(path) if path && File.exist?(path)
262
+ end
263
+
214
264
  def wait_for_callback(login, timeout_seconds:)
215
265
  code = login.oauth.wait_for_login_callback(login.server, expected_state: login.state, timeout_seconds: timeout_seconds)
216
266
  complete_login(login, code) unless code.to_s.empty?
@@ -239,7 +289,7 @@ module Kward
239
289
 
240
290
  def login_payload(login)
241
291
  {
242
- providerId: "openai",
292
+ providerId: provider_id_for_login(login),
243
293
  loginId: login.id,
244
294
  status: login.status,
245
295
  redirectUri: login.redirect_uri,
@@ -248,16 +298,20 @@ module Kward
248
298
  }.compact
249
299
  end
250
300
 
301
+ def provider_id_for_login(login)
302
+ login.provider_id || "openai"
303
+ end
304
+
251
305
  def login_status_message(status)
252
306
  case status
253
307
  when "completed"
254
- "Logged in to OpenAI."
308
+ "OAuth login completed."
255
309
  when "failed"
256
- "OpenAI login failed."
310
+ "OAuth login failed."
257
311
  when "cancelled"
258
- "OpenAI login cancelled."
312
+ "OAuth login cancelled."
259
313
  else
260
- "OpenAI login pending."
314
+ "OAuth login pending."
261
315
  end
262
316
  end
263
317
  end
@@ -3,8 +3,11 @@ require_relative "../config_files"
3
3
  require_relative "../model/model_info"
4
4
  require_relative "redactor"
5
5
 
6
+ # Namespace for the Kward CLI agent runtime.
6
7
  module Kward
8
+ # JSON-RPC backend namespace used by UI clients.
7
9
  module RPC
10
+ # RPC configuration manager for reading and updating user config.
8
11
  class ConfigManager
9
12
  def initialize(config_path: OpenAIOAuth.default_config_path)
10
13
  @config_path = File.expand_path(config_path)
@@ -48,6 +51,14 @@ module Kward
48
51
  ConfigFiles.delete_config_key(key, @config_path)
49
52
  end
50
53
 
54
+ def workspace_guardrails_enabled?
55
+ ConfigFiles.workspace_guardrails_enabled?(read(redacted: false))
56
+ end
57
+
58
+ def session_auto_resume_enabled?
59
+ ConfigFiles.session_auto_resume_enabled?(read(redacted: false))
60
+ end
61
+
51
62
  private
52
63
 
53
64
  def load_config
@@ -1,14 +1,13 @@
1
1
  require "securerandom"
2
2
  require_relative "../message_access"
3
+ require_relative "../question_contract"
3
4
 
5
+ # Namespace for the Kward CLI agent runtime.
4
6
  module Kward
7
+ # JSON-RPC backend namespace used by UI clients.
5
8
  module RPC
9
+ # RPC prompt bridge for structured user questions.
6
10
  class PromptBridge
7
- MIN_QUESTIONS = 1
8
- MAX_QUESTIONS = 4
9
- MIN_OPTIONS = 2
10
- MAX_OPTIONS = 4
11
-
12
11
  def initialize(server:, session_id:)
13
12
  @server = server
14
13
  @session_id = session_id
@@ -77,27 +76,7 @@ module Kward
77
76
  end
78
77
 
79
78
  def validate_questions(questions)
80
- raise ArgumentError, "questions must be an array" unless questions.is_a?(Array)
81
- unless questions.length.between?(MIN_QUESTIONS, MAX_QUESTIONS)
82
- raise ArgumentError, "ui/question requires 1-4 questions"
83
- end
84
-
85
- questions.each_with_index do |question, index|
86
- validate_question(question, index)
87
- end
88
- questions
89
- end
90
-
91
- def validate_question(question, index)
92
- raise ArgumentError, "question #{index + 1} must be an object" unless question.is_a?(Hash)
93
-
94
- options = MessageAccess.value(question, :options)
95
- raise ArgumentError, "question #{index + 1} options must be an array" unless options.is_a?(Array)
96
- unless options.length.between?(MIN_OPTIONS, MAX_OPTIONS)
97
- raise ArgumentError, "question #{index + 1} requires 2-4 options"
98
- end
99
- raise ArgumentError, "question #{index + 1} multiSelect is unsupported" if MessageAccess.value(question, :multiSelect) == true
100
- raise ArgumentError, "question #{index + 1} preview is unsupported" if options.any? { |option| option.is_a?(Hash) && MessageAccess.value(option, :preview) }
79
+ QuestionContract.normalize_questions(questions)
101
80
  end
102
81
  end
103
82
  end
@@ -1,5 +1,8 @@
1
+ # Namespace for the Kward CLI agent runtime.
1
2
  module Kward
3
+ # JSON-RPC backend namespace used by UI clients.
2
4
  module RPC
5
+ # Redacts sensitive configuration values before RPC responses.
3
6
  module Redactor
4
7
  SECRET_KEYS = /(?:token|secret|api[_-]?key|authorization|password|credential)/i
5
8
 
@@ -1,5 +1,8 @@
1
+ # Namespace for the Kward CLI agent runtime.
1
2
  module Kward
3
+ # JSON-RPC backend namespace used by UI clients.
2
4
  module RPC
5
+ # Builders for Tauren-compatible runtime state payloads.
3
6
  module RuntimePayloads
4
7
  module_function
5
8
 
@@ -46,7 +49,7 @@ module Kward
46
49
  toolCalls: counts[:toolCalls],
47
50
  toolResults: counts[:toolResults],
48
51
  totalMessages: counts[:totalMessages],
49
- usingSubscription: model[:provider] == "Codex",
52
+ usingSubscription: ["Codex", "Anthropic"].include?(model[:provider]),
50
53
  autoCompactionEnabled: compaction_enabled,
51
54
  autoCompactionReserveTokens: auto_compaction_reserve_tokens,
52
55
  contextUsage: context_usage
@@ -14,7 +14,9 @@ require_relative "redactor"
14
14
  require_relative "session_manager"
15
15
  require_relative "transport"
16
16
 
17
+ # Namespace for the Kward CLI agent runtime.
17
18
  module Kward
19
+ # JSON-RPC backend namespace used by UI clients.
18
20
  module RPC
19
21
  # Experimental JSON-RPC backend for UI clients.
20
22
  #
@@ -22,6 +24,13 @@ module Kward
22
24
  # exposes capabilities during `initialize`, redacts secrets in errors and
23
25
  # notifications, and coordinates auth, config, sessions, turns, tools,
24
26
  # memory, commands, and startup resources.
27
+ #
28
+ # `Server` should stay focused on protocol concerns: framing, JSON-RPC error
29
+ # codes, method dispatch, capability reporting, and redaction at the wire
30
+ # boundary. Delegate stateful product behavior to manager objects such as
31
+ # `SessionManager`, `AuthManager`, and `ConfigManager`. When adding an RPC
32
+ # feature, update dispatch, capabilities, docs, and tests together so clients
33
+ # can trust `initialize` as the source of supported behavior.
25
34
  class Server
26
35
  PROTOCOL_VERSION = 1
27
36
  JSONRPC_VERSION = "2.0"
@@ -34,12 +43,30 @@ module Kward
34
43
  invalid_params: -32_602,
35
44
  internal_error: -32_603
36
45
  }.freeze
37
-
46
+ SESSION_METHODS = [
47
+ "sessions/create", "sessions/resume", "sessions/list", "sessions/rename",
48
+ "sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork",
49
+ "sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
50
+ "sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
51
+ ].freeze
52
+ MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set", "openrouter/catalog"].freeze
53
+ AUTH_METHODS = [
54
+ "auth/status", "auth/providers", "auth/loginWithApiKey", "auth/logoutProvider",
55
+ "auth/loginWithOAuth", "auth/startOpenAILogin", "auth/submitOpenAICode", "auth/loginStatus"
56
+ ].freeze
57
+ MEMORY_METHODS = [
58
+ "memory/status", "memory/enable", "memory/disable", "memory/autoSummary/enable",
59
+ "memory/autoSummary/disable", "memory/list", "memory/add", "memory/addCore",
60
+ "memory/forget", "memory/promote", "memory/relax", "memory/inspect",
61
+ "memory/why", "memory/summarize"
62
+ ].freeze
63
+
64
+ # Creates the RPC server and its stateful managers.
38
65
  def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new)
39
66
  @transport = Transport.new(input: input, output: output)
40
67
  @error_output = error_output
41
- @session_manager = SessionManager.new(server: self, client: client)
42
68
  @config_manager = ConfigManager.new
69
+ @session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
43
70
  @auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
44
71
  @shutdown = false
45
72
  end
@@ -218,7 +245,7 @@ module Kward
218
245
  when "sessions/resume"
219
246
  @session_manager.resume_session(path: params.fetch("path"), workspace_root: params["workspaceRoot"])
220
247
  when "sessions/list"
221
- { sessions: @session_manager.list_sessions(workspace_root: params["workspaceRoot"] || Dir.pwd, limit: params["limit"]) }
248
+ { sessions: @session_manager.list_sessions(workspace_root: params["workspaceRoot"] || Dir.pwd, limit: params["limit"], current_session_path: params["currentSessionPath"]) }
222
249
  when "sessions/rename"
223
250
  @session_manager.rename_session(session_id: params.fetch("sessionId"), name: params["name"])
224
251
  when "sessions/clone"
@@ -287,7 +314,7 @@ module Kward
287
314
  sessions: {
288
315
  mode: "explicit",
289
316
  persistence: "jsonl",
290
- methods: ["sessions/create", "sessions/resume", "sessions/list", "sessions/rename", "sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork", "sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate", "sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"],
317
+ methods: SESSION_METHODS,
291
318
  startupResume: { supported: true, method: "sessions/create", parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
292
319
  list: { supported: true, source: "rpc", ancestry: true, treeFields: true },
293
320
  fork: { supported: true, methods: ["sessions/forkMessages", "sessions/fork"], entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
@@ -333,7 +360,7 @@ module Kward
333
360
  },
334
361
  models: {
335
362
  supported: true,
336
- methods: ["models/list", "models/current", "models/set", "reasoning/set", "openrouter/catalog"],
363
+ methods: MODEL_METHODS,
337
364
  fields: ["provider", "id", "name", "reasoning", "reasoningEffort", "contextWindow"],
338
365
  scopedModels: false
339
366
  },
@@ -351,13 +378,13 @@ module Kward
351
378
  auth: {
352
379
  supported: true,
353
380
  providerFormat: "tauren-auth-v1",
354
- methods: ["auth/status", "auth/providers", "auth/loginWithApiKey", "auth/logoutProvider", "auth/loginWithOAuth", "auth/startOpenAILogin", "auth/submitOpenAICode", "auth/loginStatus"],
355
- oauthProviders: ["openai", "github"],
381
+ methods: AUTH_METHODS,
382
+ oauthProviders: ["openai", "anthropic", "github"],
356
383
  unsupportedOAuthProviders: { github: "CLI-only GitHub login for Copilot scaffolding; RPC login is not implemented yet." },
357
384
  apiKeyProviders: ["openrouter"],
358
385
  logout: true
359
386
  },
360
- memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: ["memory/status", "memory/enable", "memory/disable", "memory/autoSummary/enable", "memory/autoSummary/disable", "memory/list", "memory/add", "memory/addCore", "memory/forget", "memory/promote", "memory/relax", "memory/inspect", "memory/why", "memory/summarize"] },
387
+ memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
361
388
  commands: { supported: true, methods: ["commands/list", "commands/run"], method: "commands/list", runMethod: "commands/run", sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
362
389
  startupResources: { supported: true, method: "resources/startup" },
363
390
  starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
@@ -600,11 +627,11 @@ module Kward
600
627
  end
601
628
 
602
629
  def workspace_guardrails_enabled?
603
- ConfigFiles.workspace_guardrails_enabled?(@config_manager.read(redacted: false))
630
+ @config_manager.workspace_guardrails_enabled?
604
631
  end
605
632
 
606
633
  def session_auto_resume_enabled?
607
- ConfigFiles.session_auto_resume_enabled?(@config_manager.read(redacted: false))
634
+ @config_manager.session_auto_resume_enabled?
608
635
  end
609
636
 
610
637
  def write_result(id, result)