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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +2 -2
- data/README.md +5 -5
- data/doc/authentication.md +24 -1
- data/doc/configuration.md +9 -2
- data/doc/extensibility.md +1 -1
- data/doc/getting-started.md +4 -6
- data/doc/plugins.md +0 -2
- data/doc/releasing.md +7 -8
- data/doc/rpc.md +6 -6
- data/doc/usage.md +5 -2
- data/doc/web-search.md +2 -2
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +29 -2
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +222 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +225 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +132 -0
- data/lib/kward/cli/rendering.rb +389 -0
- data/lib/kward/cli/runtime_helpers.rb +159 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +663 -0
- data/lib/kward/cli/slash_commands.rb +112 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/tool_summaries.rb +153 -0
- data/lib/kward/cli.rb +38 -2790
- data/lib/kward/cli_transcript_formatter.rb +4 -7
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +29 -7
- data/lib/kward/config_files.rb +33 -24
- data/lib/kward/conversation.rb +70 -5
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +13 -0
- data/lib/kward/message_access.rb +23 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +3 -0
- data/lib/kward/model/model_info.rb +143 -4
- data/lib/kward/model/payloads.rb +166 -13
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +129 -0
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +37 -10
- data/lib/kward/rpc/session_manager.rb +123 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +3 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +125 -31
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +1 -0
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +33 -2
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +17 -14
- data/lib/kward/tools/tool_call.rb +25 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- 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
|
@@ -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],
|
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
"
|
|
308
|
+
"OAuth login completed."
|
|
255
309
|
when "failed"
|
|
256
|
-
"
|
|
310
|
+
"OAuth login failed."
|
|
257
311
|
when "cancelled"
|
|
258
|
-
"
|
|
312
|
+
"OAuth login cancelled."
|
|
259
313
|
else
|
|
260
|
-
"
|
|
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
|
-
|
|
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
|
data/lib/kward/rpc/redactor.rb
CHANGED
|
@@ -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]
|
|
52
|
+
usingSubscription: ["Codex", "Anthropic"].include?(model[:provider]),
|
|
50
53
|
autoCompactionEnabled: compaction_enabled,
|
|
51
54
|
autoCompactionReserveTokens: auto_compaction_reserve_tokens,
|
|
52
55
|
contextUsage: context_usage
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
630
|
+
@config_manager.workspace_guardrails_enabled?
|
|
604
631
|
end
|
|
605
632
|
|
|
606
633
|
def session_auto_resume_enabled?
|
|
607
|
-
|
|
634
|
+
@config_manager.session_auto_resume_enabled?
|
|
608
635
|
end
|
|
609
636
|
|
|
610
637
|
def write_result(id, result)
|