kward 0.67.1 → 0.69.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/pages.yml +48 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile.lock +8 -2
- data/README.md +37 -30
- data/Rakefile +14 -1
- data/doc/authentication.md +84 -43
- data/doc/code-search.md +55 -28
- data/doc/configuration.md +27 -2
- data/doc/extensibility.md +90 -129
- data/doc/getting-started.md +53 -57
- data/doc/memory.md +51 -118
- data/doc/personas.md +417 -0
- data/doc/plugins.md +55 -99
- data/doc/releasing.md +10 -9
- data/doc/rpc.md +7 -7
- data/doc/usage.md +125 -141
- data/doc/web-search.md +80 -14
- data/exe/kward +2 -0
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +30 -3
- 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 +229 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +227 -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 +134 -0
- data/lib/kward/cli/rendering.rb +378 -0
- data/lib/kward/cli/runtime_helpers.rb +170 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +669 -0
- data/lib/kward/cli/slash_commands.rb +114 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/sysprompt.rb +57 -0
- data/lib/kward/cli/tool_summaries.rb +157 -0
- data/lib/kward/cli.rb +52 -2792
- data/lib/kward/cli_transcript_formatter.rb +40 -12
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +31 -9
- data/lib/kward/config_files.rb +78 -34
- data/lib/kward/conversation.rb +110 -13
- 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 +144 -14
- data/lib/kward/message_access.rb +29 -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 +14 -10
- data/lib/kward/model/model_info.rb +160 -4
- data/lib/kward/model/payloads.rb +254 -22
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +387 -25
- 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 +151 -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 +63 -7
- 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 +43 -11
- data/lib/kward/rpc/session_manager.rb +139 -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 +50 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +154 -25
- 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 +3 -2
- 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/fetch_content.rb +41 -0
- data/lib/kward/tools/fetch_raw.rb +40 -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 +42 -4
- 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 +20 -17
- data/lib/kward/tools/search/web_fetch.rb +202 -0
- data/lib/kward/tools/tool_call.rb +27 -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
- data/templates/default/fulldoc/html/css/kward.css +1501 -0
- data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
- data/templates/default/fulldoc/html/js/kward.js +296 -0
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/layout/html/breadcrumb.erb +11 -0
- data/templates/default/layout/html/layout.erb +141 -0
- data/templates/default/layout/html/setup.rb +139 -0
- metadata +56 -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
|
|
|
@@ -12,18 +14,27 @@ module Kward
|
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context).map { |section| section[:content] }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
|
|
21
|
+
sections = [prompt_section("Built-in system prompt", base_prompt)]
|
|
22
|
+
sections << prompt_section(config_agents_prompt_label, config_agents_prompt, source: config_agents_prompt_source)
|
|
23
|
+
sections << prompt_section("Memory context", memory_context) unless memory_context.to_s.empty?
|
|
24
|
+
if include_workspace_personality
|
|
25
|
+
sections << prompt_section("Persona", persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now))
|
|
26
|
+
sections << prompt_section("Plugin context", plugin_context) unless plugin_context.to_s.empty?
|
|
27
|
+
end
|
|
28
|
+
sections << prompt_section("Configured skills", skills_prompt, source: ConfigFiles.skills.empty? ? nil : File.join(ConfigFiles.config_dir, "skills"))
|
|
29
|
+
sections << prompt_section(workspace_agents_context_label(workspace_root), workspace_agents_context(workspace_root), source: ConfigFiles.workspace_agents_file?(workspace_root) ? ConfigFiles.workspace_agents_path(workspace_root) : nil)
|
|
30
|
+
sections.compact
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
def base_prompt
|
|
25
34
|
<<~PROMPT.strip
|
|
26
35
|
You are Kward, a concise practical CLI coding agent. You are allowed to use the tools. Help users understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
|
|
36
|
+
|
|
37
|
+
For web research, use web_search to discover sources, then fetch_content for important human-readable pages before relying on them. Use fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources when practical, and cite or mention the URLs you relied on.
|
|
27
38
|
PROMPT
|
|
28
39
|
end
|
|
29
40
|
|
|
@@ -31,14 +42,59 @@ module Kward
|
|
|
31
42
|
ConfigFiles.agents_prompt
|
|
32
43
|
end
|
|
33
44
|
|
|
45
|
+
def config_agents_prompt_label
|
|
46
|
+
return "Config principles" if File.exist?(ConfigFiles.config_principles_path)
|
|
47
|
+
return "Config AGENTS.md alias" if File.exist?(ConfigFiles.config_agents_path)
|
|
48
|
+
|
|
49
|
+
"Config principles"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config_agents_prompt_source
|
|
53
|
+
return ConfigFiles.config_principles_path if File.exist?(ConfigFiles.config_principles_path)
|
|
54
|
+
return ConfigFiles.config_agents_path if File.exist?(ConfigFiles.config_agents_path)
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
34
59
|
def persona_prompt(workspace_root = Dir.pwd, model: nil, reasoning_effort: nil, now: Time.now)
|
|
35
60
|
ConfigFiles.persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now)
|
|
36
61
|
end
|
|
37
62
|
|
|
63
|
+
def workspace_agents_context(workspace_root = Dir.pwd)
|
|
64
|
+
if ConfigFiles.enforce_workspace_agents_file?
|
|
65
|
+
ConfigFiles.workspace_agents_prompt(workspace_root)
|
|
66
|
+
else
|
|
67
|
+
workspace_agents_hint(workspace_root)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
38
71
|
def workspace_agents_prompt(workspace_root = Dir.pwd)
|
|
39
72
|
ConfigFiles.workspace_agents_prompt(workspace_root)
|
|
40
73
|
end
|
|
41
74
|
|
|
75
|
+
def workspace_agents_hint(workspace_root = Dir.pwd)
|
|
76
|
+
return nil unless ConfigFiles.workspace_agents_file?(workspace_root)
|
|
77
|
+
|
|
78
|
+
path = ConfigFiles.workspace_agents_path(workspace_root)
|
|
79
|
+
<<~PROMPT.strip
|
|
80
|
+
Workspace guidance is available in AGENTS.md at the workspace root: #{path}
|
|
81
|
+
For tasks involving this repository, read it before analyzing or modifying project files, and follow it when it does not conflict with higher-priority instructions or the user's request.
|
|
82
|
+
PROMPT
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def workspace_agents_context_label(workspace_root = Dir.pwd)
|
|
86
|
+
return "Workspace AGENTS.md" unless ConfigFiles.workspace_agents_file?(workspace_root)
|
|
87
|
+
return "Workspace AGENTS.md" if ConfigFiles.enforce_workspace_agents_file?
|
|
88
|
+
|
|
89
|
+
"Workspace AGENTS.md hint"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def prompt_section(label, content, source: nil)
|
|
93
|
+
return nil if content.to_s.empty?
|
|
94
|
+
|
|
95
|
+
{ label: label, content: content, source: source }
|
|
96
|
+
end
|
|
97
|
+
|
|
42
98
|
def skills_prompt
|
|
43
99
|
skills = ConfigFiles.skills
|
|
44
100
|
return nil if skills.empty?
|
|
@@ -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
|