kward 0.74.0 → 0.75.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 +35 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -2
- data/Rakefile +18 -0
- data/doc/configuration.md +56 -3
- data/doc/extensibility.md +6 -19
- data/doc/getting-started.md +1 -1
- data/doc/lifecycle-hooks.md +424 -0
- data/doc/memory.md +1 -1
- data/doc/personas.md +2 -2
- data/doc/plugins.md +1 -1
- data/doc/releasing.md +7 -15
- data/doc/rpc.md +108 -16
- data/doc/session-management.md +9 -9
- data/doc/skills.md +199 -0
- data/doc/usage.md +7 -7
- data/lib/kward/agent.rb +72 -22
- data/lib/kward/auth/anthropic_oauth.rb +5 -87
- data/lib/kward/auth/oauth_helpers.rb +105 -0
- data/lib/kward/auth/openai_oauth.rb +4 -86
- data/lib/kward/cli/commands.rb +9 -2
- data/lib/kward/cli/compaction.rb +7 -0
- data/lib/kward/cli/git.rb +30 -3
- data/lib/kward/cli/hook_commands.rb +258 -0
- data/lib/kward/cli/memory_commands.rb +1 -5
- data/lib/kward/cli/plugins.rb +41 -1
- data/lib/kward/cli/prompt_interface.rb +32 -3
- data/lib/kward/cli/rendering.rb +1 -1
- data/lib/kward/cli/runtime_helpers.rb +44 -5
- data/lib/kward/cli/sessions.rb +13 -0
- data/lib/kward/cli/settings.rb +226 -152
- data/lib/kward/cli/slash_commands.rb +81 -42
- data/lib/kward/cli/tabs.rb +26 -6
- data/lib/kward/cli.rb +26 -3
- data/lib/kward/cli_transcript_formatter.rb +0 -7
- data/lib/kward/compaction/token_estimator.rb +112 -0
- data/lib/kward/compactor.rb +42 -116
- data/lib/kward/config_files.rb +117 -25
- data/lib/kward/deep_copy.rb +42 -0
- data/lib/kward/diff_view_mode.rb +36 -0
- data/lib/kward/events.rb +1 -0
- data/lib/kward/export_path.rb +2 -6
- data/lib/kward/frontmatter.rb +43 -0
- data/lib/kward/hooks/audit_log.rb +121 -0
- data/lib/kward/hooks/catalog.rb +110 -0
- data/lib/kward/hooks/command_handler.rb +66 -0
- data/lib/kward/hooks/config_loader.rb +104 -0
- data/lib/kward/hooks/decision.rb +102 -0
- data/lib/kward/hooks/event.rb +60 -0
- data/lib/kward/hooks/http_handler.rb +72 -0
- data/lib/kward/hooks/manager.rb +158 -0
- data/lib/kward/hooks/matcher.rb +75 -0
- data/lib/kward/hooks.rb +16 -0
- data/lib/kward/markdown_transcript.rb +4 -3
- data/lib/kward/mcp/client.rb +1 -1
- data/lib/kward/mcp/server_config.rb +7 -3
- data/lib/kward/mcp/stdio_transport.rb +7 -1
- data/lib/kward/memory/turn_context.rb +19 -0
- data/lib/kward/message_access.rb +2 -2
- data/lib/kward/message_text.rb +19 -0
- data/lib/kward/model/client.rb +13 -45
- data/lib/kward/model/context_usage.rb +10 -4
- data/lib/kward/model/copilot_models.rb +44 -0
- data/lib/kward/model/model_info.rb +24 -0
- data/lib/kward/model/payloads.rb +15 -17
- data/lib/kward/model/stream_parser.rb +178 -13
- data/lib/kward/pan/server.rb +51 -2
- data/lib/kward/path_guard.rb +13 -0
- data/lib/kward/plugin_registry.rb +86 -25
- data/lib/kward/prompt_interface/editor/controller.rb +60 -1
- data/lib/kward/prompt_interface/editor/renderer.rb +11 -0
- data/lib/kward/prompt_interface/editor/state.rb +5 -1
- data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
- data/lib/kward/prompt_interface.rb +4 -1
- data/lib/kward/prompts/commands.rb +3 -3
- data/lib/kward/prompts.rb +7 -7
- data/lib/kward/rpc/mcp_status.rb +50 -0
- data/lib/kward/rpc/prompt_bridge.rb +38 -4
- data/lib/kward/rpc/runtime_payloads.rb +1 -1
- data/lib/kward/rpc/server.rb +94 -14
- data/lib/kward/rpc/session_manager.rb +258 -33
- data/lib/kward/rpc/session_metrics.rb +1 -1
- data/lib/kward/rpc/session_tree_rows.rb +13 -64
- data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
- data/lib/kward/rpc/transcript_normalizer.rb +9 -10
- data/lib/kward/rpc/turn_context.rb +109 -0
- data/lib/kward/session_store.rb +6 -20
- data/lib/kward/session_tree_nodes.rb +98 -18
- data/lib/kward/session_tree_renderer.rb +6 -61
- data/lib/kward/skills/registry.rb +109 -17
- data/lib/kward/tools/mcp_tool.rb +4 -0
- data/lib/kward/tools/read_skill.rb +9 -4
- data/lib/kward/tools/registry.rb +266 -8
- data/lib/kward/tools/search/code.rb +4 -22
- data/lib/kward/tools/search/web.rb +28 -2
- data/lib/kward/update_check.rb +117 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/manager.rb +43 -3
- data/lib/kward/workspace.rb +3 -1
- data/templates/default/fulldoc/html/css/kward.css +22 -0
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +4 -2
- data/templates/default/layout/html/setup.rb +2 -0
- metadata +25 -1
|
@@ -73,6 +73,8 @@ module Kward
|
|
|
73
73
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
74
74
|
|
|
75
75
|
case code
|
|
76
|
+
when 9
|
|
77
|
+
modified_tab_sequence?(sequence) ? true : select_toggle_search
|
|
76
78
|
when 13
|
|
77
79
|
select_input_active? ? select_input_action_result : select_current_choice
|
|
78
80
|
when 27
|
|
@@ -501,25 +503,43 @@ module Kward
|
|
|
501
503
|
|
|
502
504
|
def select_typed_key(key)
|
|
503
505
|
return select_insert_key(key) if select_input_active?
|
|
504
|
-
return
|
|
506
|
+
return select_toggle_search if select_search_toggle_key?(key)
|
|
505
507
|
return select_action_key(key) unless select_search_active?
|
|
506
508
|
|
|
507
509
|
select_insert_key(key)
|
|
508
510
|
end
|
|
509
511
|
|
|
512
|
+
def select_search_toggle_key?(key)
|
|
513
|
+
TerminalKeys::TAB.include?(key)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def modified_tab_sequence?(sequence)
|
|
517
|
+
modifier = sequence[:modifier]
|
|
518
|
+
modifier && modifier != 1
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def select_toggle_search
|
|
522
|
+
select_search_active? ? select_pause_search : select_begin_search
|
|
523
|
+
end
|
|
524
|
+
|
|
510
525
|
def select_begin_search
|
|
511
526
|
return unless @select_state
|
|
512
527
|
|
|
513
528
|
@select_state[:search_active] = true
|
|
514
|
-
self.composer_input = ""
|
|
515
|
-
self.composer_cursor = 0
|
|
516
529
|
true
|
|
517
530
|
end
|
|
518
531
|
|
|
519
|
-
def
|
|
532
|
+
def select_pause_search
|
|
520
533
|
return unless @select_state
|
|
521
534
|
|
|
522
535
|
@select_state[:search_active] = false
|
|
536
|
+
true
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def select_cancel_search
|
|
540
|
+
return unless @select_state
|
|
541
|
+
|
|
542
|
+
select_pause_search
|
|
523
543
|
self.composer_input = ""
|
|
524
544
|
self.composer_cursor = 0
|
|
525
545
|
@select_state[:selection_index] = 0
|
|
@@ -607,7 +627,7 @@ module Kward
|
|
|
607
627
|
|
|
608
628
|
def selection_matches
|
|
609
629
|
choices = @select_state ? @select_state[:choices] : []
|
|
610
|
-
filter = select_search_active? ? composer_input.downcase.strip : ""
|
|
630
|
+
filter = (select_search_active? || !composer_input.strip.empty?) ? composer_input.downcase.strip : ""
|
|
611
631
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
612
632
|
clamp_selection_index(matches.length)
|
|
613
633
|
matches
|
|
@@ -666,7 +686,7 @@ module Kward
|
|
|
666
686
|
return "Renaming · Enter save · Esc cancel" if select_input_active?
|
|
667
687
|
|
|
668
688
|
text = "↑/↓ select · Enter open"
|
|
669
|
-
text = "#{text} ·
|
|
689
|
+
text = "#{text} · Tab search" unless select_search_active?
|
|
670
690
|
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
671
691
|
action_keys.each do |key, action|
|
|
672
692
|
text = "#{text} · #{key} #{action[:action]}"
|
|
@@ -12,6 +12,7 @@ require_relative "ansi"
|
|
|
12
12
|
require_relative "terminal_sequences"
|
|
13
13
|
require_relative "terminal_keys"
|
|
14
14
|
require_relative "editor_mode"
|
|
15
|
+
require_relative "diff_view_mode"
|
|
15
16
|
require_relative "prompt_interface/banner"
|
|
16
17
|
require_relative "prompt_interface/composer_state"
|
|
17
18
|
require_relative "prompt_interface/editor/state"
|
|
@@ -125,7 +126,7 @@ module Kward
|
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
|
|
128
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil)
|
|
129
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil)
|
|
129
130
|
@input_io = input
|
|
130
131
|
@output_io = output
|
|
131
132
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
@@ -206,6 +207,8 @@ module Kward
|
|
|
206
207
|
@editor_bar_cursor_source = editor_bar_cursor_source
|
|
207
208
|
@editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
|
|
208
209
|
@editor_line_numbers_source = editor_line_numbers_source
|
|
210
|
+
@diff_view = DiffViewMode.normalize(diff_view)
|
|
211
|
+
@diff_view_source = diff_view_source
|
|
209
212
|
end
|
|
210
213
|
|
|
211
214
|
def start(render: true)
|
|
@@ -8,9 +8,8 @@ module Kward
|
|
|
8
8
|
{ name: "exit", description: "Exit the interactive session.", argument_hint: "" },
|
|
9
9
|
{ name: "quit", description: "Exit the interactive session.", argument_hint: "" },
|
|
10
10
|
{ name: "new", description: "Start a new session.", argument_hint: "" },
|
|
11
|
-
{ name: "
|
|
12
|
-
{ name: "resume", description: "Alias for /
|
|
13
|
-
{ name: "name", description: "Name or clear the current session.", argument_hint: "[name]" },
|
|
11
|
+
{ name: "session", description: "Open saved sessions or name the current session.", argument_hint: "[path]|name [name]" },
|
|
12
|
+
{ name: "resume", description: "Alias for /session.", argument_hint: "[path]" },
|
|
14
13
|
{ name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
|
|
15
14
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
16
15
|
{ name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
|
|
@@ -36,6 +35,7 @@ module Kward
|
|
|
36
35
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
|
|
37
36
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
38
37
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
38
|
+
{ name: "hooks", description: "Inspect lifecycle hooks.", argument_hint: "[list|events|logs|doctor|trust|untrust]" },
|
|
39
39
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|
|
40
40
|
].freeze
|
|
41
41
|
BUILTIN_RESERVED_COMMAND_NAMES = BUILTIN_COMMANDS.map { |command| command[:name] }.freeze
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Kward
|
|
|
25
25
|
sections << prompt_section("Persona", persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now))
|
|
26
26
|
sections << prompt_section("Plugin context", plugin_context) unless plugin_context.to_s.empty?
|
|
27
27
|
end
|
|
28
|
-
|
|
28
|
+
skills = ConfigFiles.skills(workspace_root: workspace_root)
|
|
29
|
+
sections << prompt_section("Configured skills", skills_prompt(skills), source: skills.empty? ? nil : File.join(ConfigFiles.config_dir, "skills"))
|
|
29
30
|
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
31
|
sections.compact
|
|
31
32
|
end
|
|
@@ -97,18 +98,17 @@ module Kward
|
|
|
97
98
|
{ label: label, content: content, source: source }
|
|
98
99
|
end
|
|
99
100
|
|
|
100
|
-
def skills_prompt
|
|
101
|
-
skills = ConfigFiles.skills
|
|
101
|
+
def skills_prompt(skills = ConfigFiles.skills)
|
|
102
102
|
return nil if skills.empty?
|
|
103
103
|
|
|
104
104
|
lines = [
|
|
105
|
-
"
|
|
106
|
-
"When a task matches a skill, use read_skill to load its instructions before proceeding.",
|
|
105
|
+
"Agent Skills are available.",
|
|
106
|
+
"When a task matches a skill's description, use read_skill to load its instructions before proceeding.",
|
|
107
|
+
"Use read_skill with a relative path to read referenced files inside a skill directory.",
|
|
107
108
|
"Available skills:"
|
|
108
109
|
]
|
|
109
110
|
skills.each do |skill|
|
|
110
|
-
|
|
111
|
-
lines << "- #{skill.name}: #{description}"
|
|
111
|
+
lines << "- #{skill.name}: #{skill.description}"
|
|
112
112
|
end
|
|
113
113
|
lines.join("\n")
|
|
114
114
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative "../mcp/server_config"
|
|
2
|
+
require_relative "redactor"
|
|
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
|
+
# Builds RPC-visible MCP server status payloads.
|
|
9
|
+
class MCPStatus
|
|
10
|
+
def initialize(config_manager:)
|
|
11
|
+
@config_manager = config_manager
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
config = @config_manager.read(redacted: false)
|
|
16
|
+
clients = MCP::ServerConfig.clients_from_config(config).to_h { |client| [client.name, client] }
|
|
17
|
+
servers = MCP::ServerConfig.configured_servers(config).map do |name, settings|
|
|
18
|
+
server_status(name, settings, clients[name.to_s])
|
|
19
|
+
end
|
|
20
|
+
{ servers: servers }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def server_status(name, settings, client)
|
|
26
|
+
base = { name: name.to_s, transport: "stdio", toolCount: 0 }
|
|
27
|
+
unless client
|
|
28
|
+
return base.merge(status: "unavailable", error: "command not configured") if settings["command"].to_s.empty?
|
|
29
|
+
|
|
30
|
+
return base.merge(status: "unavailable", error: "unsupported MCP server configuration")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
tools = client.list_tools
|
|
34
|
+
base.merge(status: "available", toolCount: tools.length)
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
base.merge(status: "unavailable", toolCount: 0, error: redacted_error(e.message, client))
|
|
37
|
+
ensure
|
|
38
|
+
client&.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def redacted_error(message, client)
|
|
42
|
+
text = Redactor.redact_string(message.to_s)
|
|
43
|
+
command = client&.transport&.respond_to?(:command) ? client.transport.command.to_s : ""
|
|
44
|
+
return text if command.empty?
|
|
45
|
+
|
|
46
|
+
text.gsub(command, File.basename(command))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -17,6 +17,32 @@ module Kward
|
|
|
17
17
|
@pending_requests = {}
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def request_tool_approval(tool_call_id:, tool_name:, args:, cancellation: nil)
|
|
21
|
+
request_id = SecureRandom.uuid
|
|
22
|
+
@mutex.synchronize { @pending_requests[request_id] = true }
|
|
23
|
+
cancellation&.on_cancel { cancel_request(request_id) }
|
|
24
|
+
unless cancellation&.cancelled?
|
|
25
|
+
@server.notify("tool/approvalRequested", {
|
|
26
|
+
sessionId: @session_id,
|
|
27
|
+
approvalRequestId: request_id,
|
|
28
|
+
toolCallId: tool_call_id,
|
|
29
|
+
toolName: tool_name,
|
|
30
|
+
args: args
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@condition.wait(@mutex) until @answers.key?(request_id)
|
|
36
|
+
answer = @answers.delete(request_id)
|
|
37
|
+
@pending_requests.delete(request_id)
|
|
38
|
+
approved_answer?(answer)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def answer_tool_approval(request_id, approved:)
|
|
43
|
+
answer_raw(request_id, !!approved)
|
|
44
|
+
end
|
|
45
|
+
|
|
20
46
|
def ask_user_question(questions, cancellation: nil)
|
|
21
47
|
questions = validate_questions(questions)
|
|
22
48
|
request_id = SecureRandom.uuid
|
|
@@ -41,28 +67,36 @@ module Kward
|
|
|
41
67
|
end
|
|
42
68
|
|
|
43
69
|
def answer(request_id, answers)
|
|
70
|
+
answer_raw(request_id, normalize_answers(answers))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cancel_request(request_id)
|
|
44
74
|
@mutex.synchronize do
|
|
45
75
|
request_id = request_id.to_s
|
|
46
76
|
return unless @pending_requests.key?(request_id)
|
|
47
77
|
return if @answers.key?(request_id)
|
|
48
78
|
|
|
49
|
-
@answers[request_id] =
|
|
79
|
+
@answers[request_id] = nil
|
|
50
80
|
@condition.broadcast
|
|
51
81
|
end
|
|
52
82
|
end
|
|
53
83
|
|
|
54
|
-
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def answer_raw(request_id, value)
|
|
55
87
|
@mutex.synchronize do
|
|
56
88
|
request_id = request_id.to_s
|
|
57
89
|
return unless @pending_requests.key?(request_id)
|
|
58
90
|
return if @answers.key?(request_id)
|
|
59
91
|
|
|
60
|
-
@answers[request_id] =
|
|
92
|
+
@answers[request_id] = value
|
|
61
93
|
@condition.broadcast
|
|
62
94
|
end
|
|
63
95
|
end
|
|
64
96
|
|
|
65
|
-
|
|
97
|
+
def approved_answer?(answer)
|
|
98
|
+
answer == true
|
|
99
|
+
end
|
|
66
100
|
|
|
67
101
|
def normalize_answers(answers)
|
|
68
102
|
return nil if answers.nil?
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "../telemetry/logger"
|
|
|
11
11
|
require_relative "../telemetry/stats"
|
|
12
12
|
require_relative "auth_manager"
|
|
13
13
|
require_relative "config_manager"
|
|
14
|
+
require_relative "mcp_status"
|
|
14
15
|
require_relative "redactor"
|
|
15
16
|
require_relative "session_manager"
|
|
16
17
|
require_relative "transport"
|
|
@@ -19,7 +20,7 @@ require_relative "transport"
|
|
|
19
20
|
module Kward
|
|
20
21
|
# JSON-RPC backend namespace used by UI clients.
|
|
21
22
|
module RPC
|
|
22
|
-
#
|
|
23
|
+
# JSON-RPC backend for trusted local UI clients.
|
|
23
24
|
#
|
|
24
25
|
# The server speaks LSP-style Content-Length framing over stdin/stdout,
|
|
25
26
|
# exposes capabilities during `initialize`, redacts secrets in errors and
|
|
@@ -47,14 +48,15 @@ module Kward
|
|
|
47
48
|
PROTOCOL_METHODS = ["initialize", "shutdown"].freeze
|
|
48
49
|
WORKSPACE_METHODS = ["workspace/validate", "workspace/info"].freeze
|
|
49
50
|
TOOL_METHODS = ["tools/list"].freeze
|
|
51
|
+
MCP_METHODS = ["mcp/status"].freeze
|
|
50
52
|
PROMPT_METHODS = ["prompts/list", "prompts/expand"].freeze
|
|
51
53
|
SESSION_METHODS = [
|
|
52
54
|
"sessions/create", "sessions/resume", "sessions/list", "sessions/rename",
|
|
53
55
|
"sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork",
|
|
54
56
|
"sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
|
|
55
|
-
"sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
|
|
57
|
+
"sessions/export", "sessions/delete", "sessions/close", "sessions/transcript", "sessions/active"
|
|
56
58
|
].freeze
|
|
57
|
-
TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events"].freeze
|
|
59
|
+
TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events", "turns/list", "turns/listActive"].freeze
|
|
58
60
|
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
|
|
59
61
|
RUNTIME_METHODS = ["runtime/state", "runtime/stats"].freeze
|
|
60
62
|
RUNTIME_SETTING_METHODS = ["runtime/updateSetting", "runtime/reload"].freeze
|
|
@@ -73,16 +75,21 @@ module Kward
|
|
|
73
75
|
STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
|
|
74
76
|
CONFIG_METHODS = ["config/read", "config/update"].freeze
|
|
75
77
|
LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
|
|
78
|
+
LIFECYCLE_HOOK_METHODS = ["hooks/logs"].freeze
|
|
76
79
|
UI_METHODS = ["ui/answerQuestion"].freeze
|
|
80
|
+
TOOL_APPROVAL_METHODS = ["tool/answerApproval"].freeze
|
|
77
81
|
SESSION_EVENT_NOTIFICATION = "session/event"
|
|
78
82
|
SESSION_UPDATED_NOTIFICATION = "session/updated"
|
|
79
83
|
TURN_EVENT_NOTIFICATION = "turn/event"
|
|
80
84
|
UI_QUESTION_NOTIFICATION = "ui/question"
|
|
81
85
|
UI_FOOTER_NOTIFICATION = "ui/footer"
|
|
86
|
+
TOOL_APPROVAL_NOTIFICATION = "tool/approvalRequested"
|
|
87
|
+
HOOK_EVENT_NOTIFICATION = "hook/event"
|
|
82
88
|
METHOD_GROUPS = {
|
|
83
89
|
protocol: PROTOCOL_METHODS,
|
|
84
90
|
workspace: WORKSPACE_METHODS,
|
|
85
91
|
tools: TOOL_METHODS,
|
|
92
|
+
mcp: MCP_METHODS,
|
|
86
93
|
prompts: PROMPT_METHODS,
|
|
87
94
|
sessions: SESSION_METHODS,
|
|
88
95
|
turns: TURN_METHODS,
|
|
@@ -96,7 +103,9 @@ module Kward
|
|
|
96
103
|
startup_resources: STARTUP_RESOURCE_METHODS,
|
|
97
104
|
config: CONFIG_METHODS,
|
|
98
105
|
logging: LOGGING_METHODS,
|
|
99
|
-
|
|
106
|
+
lifecycle_hooks: LIFECYCLE_HOOK_METHODS,
|
|
107
|
+
ui: UI_METHODS,
|
|
108
|
+
tool_approval: TOOL_APPROVAL_METHODS
|
|
100
109
|
}.freeze
|
|
101
110
|
RPC_METHODS = METHOD_GROUPS.values.flatten.freeze
|
|
102
111
|
|
|
@@ -201,7 +210,9 @@ module Kward
|
|
|
201
210
|
when WORKSPACE_METHODS[1]
|
|
202
211
|
workspace_info(params["workspaceRoot"] || Dir.pwd)
|
|
203
212
|
when TOOL_METHODS[0]
|
|
204
|
-
|
|
213
|
+
@session_manager.tool_schemas(session_id: params["sessionId"])
|
|
214
|
+
when MCP_METHODS[0]
|
|
215
|
+
MCPStatus.new(config_manager: @config_manager).to_h
|
|
205
216
|
when PROMPT_METHODS[0]
|
|
206
217
|
prompts_list
|
|
207
218
|
when PROMPT_METHODS[1]
|
|
@@ -236,6 +247,8 @@ module Kward
|
|
|
236
247
|
logging_stats(params)
|
|
237
248
|
when LOGGING_METHODS[1]
|
|
238
249
|
logging_token_csv(params)
|
|
250
|
+
when LIFECYCLE_HOOK_METHODS[0]
|
|
251
|
+
lifecycle_hook_logs(params)
|
|
239
252
|
when MEMORY_METHODS[0]
|
|
240
253
|
@session_manager.memory_status
|
|
241
254
|
when MEMORY_METHODS[1]
|
|
@@ -316,12 +329,16 @@ module Kward
|
|
|
316
329
|
@session_manager.close_session(session_id: params.fetch("sessionId"))
|
|
317
330
|
when SESSION_METHODS[14]
|
|
318
331
|
@session_manager.transcript(session_id: params.fetch("sessionId"))
|
|
332
|
+
when SESSION_METHODS[15]
|
|
333
|
+
@session_manager.active_sessions
|
|
319
334
|
when TURN_METHODS[0]
|
|
320
335
|
@session_manager.start_turn(
|
|
321
336
|
session_id: params.fetch("sessionId"),
|
|
322
337
|
input: params.fetch("input"),
|
|
323
338
|
streaming_behavior: params["streamingBehavior"],
|
|
324
|
-
attachments: params["attachments"] || []
|
|
339
|
+
attachments: params["attachments"] || [],
|
|
340
|
+
options: params["options"] || {},
|
|
341
|
+
context: params["context"]
|
|
325
342
|
)
|
|
326
343
|
when TURN_METHODS[1]
|
|
327
344
|
@session_manager.cancel_turn(turn_id: params.fetch("turnId"))
|
|
@@ -329,8 +346,14 @@ module Kward
|
|
|
329
346
|
@session_manager.turn_status(turn_id: params.fetch("turnId"))
|
|
330
347
|
when TURN_METHODS[3]
|
|
331
348
|
@session_manager.turn_events(turn_id: params.fetch("turnId"), after_sequence: params["afterSequence"] || 0)
|
|
349
|
+
when TURN_METHODS[4]
|
|
350
|
+
@session_manager.list_turns(session_id: params["sessionId"])
|
|
351
|
+
when TURN_METHODS[5]
|
|
352
|
+
@session_manager.list_turns(session_id: params["sessionId"], active: true)
|
|
332
353
|
when UI_METHODS[0]
|
|
333
354
|
@session_manager.answer_question(session_id: params.fetch("sessionId"), question_request_id: params.fetch("questionRequestId"), answers: params.fetch("answers"))
|
|
355
|
+
when TOOL_APPROVAL_METHODS[0]
|
|
356
|
+
@session_manager.answer_tool_approval(session_id: params.fetch("sessionId"), approval_request_id: params.fetch("approvalRequestId"), approved: params.fetch("approved"))
|
|
334
357
|
else
|
|
335
358
|
raise NoMethodError, method
|
|
336
359
|
end
|
|
@@ -340,7 +363,7 @@ module Kward
|
|
|
340
363
|
{
|
|
341
364
|
protocolVersion: PROTOCOL_VERSION,
|
|
342
365
|
serverName: "kward",
|
|
343
|
-
experimental:
|
|
366
|
+
experimental: false,
|
|
344
367
|
capabilities: capabilities
|
|
345
368
|
}
|
|
346
369
|
end
|
|
@@ -349,7 +372,7 @@ module Kward
|
|
|
349
372
|
{
|
|
350
373
|
framing: "content-length",
|
|
351
374
|
transcript: {
|
|
352
|
-
format: "
|
|
375
|
+
format: "kward-transcript-v1",
|
|
353
376
|
messagesNormalized: true,
|
|
354
377
|
supportsImages: true,
|
|
355
378
|
supportsToolCalls: true,
|
|
@@ -363,10 +386,11 @@ module Kward
|
|
|
363
386
|
methods: SESSION_METHODS,
|
|
364
387
|
startupResume: { supported: true, method: SESSION_METHODS[0], parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
|
|
365
388
|
list: { supported: true, source: "rpc", ancestry: true, treeFields: true },
|
|
389
|
+
active: { supported: true, method: SESSION_METHODS[15] },
|
|
366
390
|
fork: { supported: true, methods: SESSION_METHODS.values_at(6, 7), entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
|
|
367
391
|
compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
|
|
368
392
|
import: { supported: false },
|
|
369
|
-
tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "
|
|
393
|
+
tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "kward-tree-items-v1" },
|
|
370
394
|
updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION }
|
|
371
395
|
},
|
|
372
396
|
turns: {
|
|
@@ -383,7 +407,21 @@ module Kward
|
|
|
383
407
|
queuedTurns: "cancel-before-run",
|
|
384
408
|
runningTurns: "stop-emitting-events-when-possible"
|
|
385
409
|
},
|
|
386
|
-
eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT }
|
|
410
|
+
eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT },
|
|
411
|
+
options: {
|
|
412
|
+
supported: true,
|
|
413
|
+
method: TURN_METHODS[0],
|
|
414
|
+
fields: ["provider", "model", "reasoningEffort", "allowedTools", "disabledTools", "approvalMode"],
|
|
415
|
+
perTurnModel: true,
|
|
416
|
+
perTurnReasoning: true,
|
|
417
|
+
perTurnToolScope: true
|
|
418
|
+
},
|
|
419
|
+
list: { supported: true, method: TURN_METHODS[4], activeMethod: TURN_METHODS[5] },
|
|
420
|
+
context: {
|
|
421
|
+
supported: true,
|
|
422
|
+
method: TURN_METHODS[0],
|
|
423
|
+
fields: ["activeFile", "openFiles", "selection", "diagnostics"]
|
|
424
|
+
}
|
|
387
425
|
},
|
|
388
426
|
events: {
|
|
389
427
|
notification: TURN_EVENT_NOTIFICATION,
|
|
@@ -391,7 +429,7 @@ module Kward
|
|
|
391
429
|
reasoning: { start: false, delta: true, end: false },
|
|
392
430
|
modelRetry: { supported: true, event: "modelRetry" },
|
|
393
431
|
steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
|
|
394
|
-
tools: { call: true, update:
|
|
432
|
+
tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
|
|
395
433
|
errors: true,
|
|
396
434
|
sessionUpdates: false
|
|
397
435
|
},
|
|
@@ -417,6 +455,20 @@ module Kward
|
|
|
417
455
|
state: { supported: true },
|
|
418
456
|
stats: { messageCounts: true, tokens: false, cost: false, contextUsage: true, contextUsageEstimated: true }
|
|
419
457
|
},
|
|
458
|
+
lifecycleHooks: {
|
|
459
|
+
supported: true,
|
|
460
|
+
events: Hooks::Catalog.event_names,
|
|
461
|
+
decisions: Hooks::Decision::VALID_DECISIONS,
|
|
462
|
+
approvals: { supported: true, via: "toolApproval", requestNotification: TOOL_APPROVAL_NOTIFICATION, answerMethod: TOOL_APPROVAL_METHODS.first },
|
|
463
|
+
notifications: [HOOK_EVENT_NOTIFICATION, "hook/message"],
|
|
464
|
+
methods: LIFECYCLE_HOOK_METHODS,
|
|
465
|
+
auditLog: { supported: true, path: File.join(ConfigFiles.config_dir, "logs", "hooks.jsonl") },
|
|
466
|
+
commandHooks: true,
|
|
467
|
+
pluginHooks: true,
|
|
468
|
+
workspaceHooks: { supported: true, trustRequired: true, trustCommand: "/hooks trust" },
|
|
469
|
+
httpHooks: { supported: true, protocol: "jsonPost" },
|
|
470
|
+
asyncHooks: { supported: true, semantics: "observeOnly" }
|
|
471
|
+
},
|
|
420
472
|
runtimeSettings: {
|
|
421
473
|
supported: true,
|
|
422
474
|
methods: RUNTIME_SETTING_METHODS,
|
|
@@ -424,7 +476,7 @@ module Kward
|
|
|
424
476
|
},
|
|
425
477
|
auth: {
|
|
426
478
|
supported: true,
|
|
427
|
-
providerFormat: "
|
|
479
|
+
providerFormat: "kward-auth-v1",
|
|
428
480
|
methods: AUTH_METHODS,
|
|
429
481
|
oauthProviders: ["openai", "anthropic", "github"],
|
|
430
482
|
unsupportedOAuthProviders: { github: "CLI-only GitHub login for Copilot scaffolding; RPC login is not implemented yet." },
|
|
@@ -433,8 +485,21 @@ module Kward
|
|
|
433
485
|
},
|
|
434
486
|
memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
|
|
435
487
|
workers: workers_capability,
|
|
488
|
+
stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: ["workers"] },
|
|
436
489
|
commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
|
|
437
|
-
mcp: {
|
|
490
|
+
mcp: {
|
|
491
|
+
supported: true,
|
|
492
|
+
transport: "stdio",
|
|
493
|
+
config: "mcpServers",
|
|
494
|
+
exposes: ["tools"],
|
|
495
|
+
discovery: {
|
|
496
|
+
supported: true,
|
|
497
|
+
methods: TOOL_METHODS + MCP_METHODS,
|
|
498
|
+
toolMetadata: true,
|
|
499
|
+
serverStatus: true
|
|
500
|
+
},
|
|
501
|
+
unsupported: ["resources", "prompts", "sampling", "streamableHttp"]
|
|
502
|
+
},
|
|
438
503
|
startupResources: { supported: true, method: STARTUP_RESOURCE_METHODS.first },
|
|
439
504
|
starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
|
|
440
505
|
shell: { supported: false, reason: "interactiveTuiOnly" },
|
|
@@ -456,7 +521,7 @@ module Kward
|
|
|
456
521
|
},
|
|
457
522
|
security: {
|
|
458
523
|
workspaceMutationGuard: "none",
|
|
459
|
-
toolApproval: "none",
|
|
524
|
+
toolApproval: { supported: true, defaultMode: "none", modes: ["none", "ask"], requestNotification: TOOL_APPROVAL_NOTIFICATION, answerMethod: TOOL_APPROVAL_METHODS.first },
|
|
460
525
|
canRunShell: true,
|
|
461
526
|
canWriteFiles: true
|
|
462
527
|
},
|
|
@@ -564,6 +629,21 @@ module Kward
|
|
|
564
629
|
raise ArgumentError, e.message == TelemetryStats::USAGE ? e.message : "#{e.message} #{TelemetryStats::USAGE}"
|
|
565
630
|
end
|
|
566
631
|
|
|
632
|
+
def lifecycle_hook_logs(params)
|
|
633
|
+
limit = params.fetch("limit", 20).to_i
|
|
634
|
+
limit = 20 unless limit.positive?
|
|
635
|
+
limit = 200 if limit > 200
|
|
636
|
+
path = File.join(ConfigFiles.config_dir, "logs", "hooks.jsonl")
|
|
637
|
+
return { path: path, records: [] } unless File.file?(path)
|
|
638
|
+
|
|
639
|
+
records = File.readlines(path, chomp: true).last(limit).filter_map do |line|
|
|
640
|
+
JSON.parse(line)
|
|
641
|
+
rescue JSON::ParserError
|
|
642
|
+
nil
|
|
643
|
+
end
|
|
644
|
+
{ path: path, records: records }
|
|
645
|
+
end
|
|
646
|
+
|
|
567
647
|
def commands_list(params)
|
|
568
648
|
@session_manager.runtime_state(session_id: params.fetch("sessionId"))
|
|
569
649
|
prompts = ConfigFiles.prompt_templates(reserved_commands: BUILTIN_SLASH_COMMAND_NAMES).map do |template|
|