kward 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +5 -5
  5. data/doc/authentication.md +24 -1
  6. data/doc/configuration.md +9 -2
  7. data/doc/extensibility.md +1 -1
  8. data/doc/getting-started.md +4 -6
  9. data/doc/plugins.md +0 -2
  10. data/doc/releasing.md +7 -8
  11. data/doc/rpc.md +6 -6
  12. data/doc/usage.md +5 -2
  13. data/doc/web-search.md +2 -2
  14. data/kward.gemspec +4 -0
  15. data/lib/kward/agent.rb +29 -2
  16. data/lib/kward/ansi.rb +3 -0
  17. data/lib/kward/auth/anthropic_oauth.rb +291 -0
  18. data/lib/kward/auth/file.rb +2 -0
  19. data/lib/kward/auth/github_oauth.rb +3 -0
  20. data/lib/kward/auth/openai_oauth.rb +4 -0
  21. data/lib/kward/auth/openrouter_api_key.rb +2 -0
  22. data/lib/kward/cancellation.rb +3 -0
  23. data/lib/kward/cli/auth_commands.rb +82 -0
  24. data/lib/kward/cli/commands.rb +222 -0
  25. data/lib/kward/cli/compaction.rb +25 -0
  26. data/lib/kward/cli/doctor.rb +121 -0
  27. data/lib/kward/cli/interactive_turn.rb +225 -0
  28. data/lib/kward/cli/memory_commands.rb +133 -0
  29. data/lib/kward/cli/plugins.rb +112 -0
  30. data/lib/kward/cli/prompt_interface.rb +132 -0
  31. data/lib/kward/cli/rendering.rb +389 -0
  32. data/lib/kward/cli/runtime_helpers.rb +159 -0
  33. data/lib/kward/cli/sessions.rb +376 -0
  34. data/lib/kward/cli/settings.rb +663 -0
  35. data/lib/kward/cli/slash_commands.rb +112 -0
  36. data/lib/kward/cli/stats.rb +64 -0
  37. data/lib/kward/cli/tool_summaries.rb +153 -0
  38. data/lib/kward/cli.rb +38 -2790
  39. data/lib/kward/cli_transcript_formatter.rb +4 -7
  40. data/lib/kward/clipboard.rb +1 -0
  41. data/lib/kward/compaction/file_operation_tracker.rb +3 -0
  42. data/lib/kward/compactor.rb +29 -7
  43. data/lib/kward/config_files.rb +33 -24
  44. data/lib/kward/conversation.rb +70 -5
  45. data/lib/kward/events.rb +2 -0
  46. data/lib/kward/export_path.rb +2 -0
  47. data/lib/kward/image_attachments.rb +2 -0
  48. data/lib/kward/markdown_transcript.rb +2 -0
  49. data/lib/kward/memory/manager.rb +13 -0
  50. data/lib/kward/message_access.rb +23 -2
  51. data/lib/kward/message_text.rb +45 -0
  52. data/lib/kward/model/chat_invocation.rb +2 -0
  53. data/lib/kward/model/client.rb +295 -77
  54. data/lib/kward/model/context_overflow.rb +2 -0
  55. data/lib/kward/model/context_usage.rb +3 -0
  56. data/lib/kward/model/model_info.rb +143 -4
  57. data/lib/kward/model/payloads.rb +166 -13
  58. data/lib/kward/model/retry_message.rb +2 -0
  59. data/lib/kward/model/stream_parser.rb +129 -0
  60. data/lib/kward/pan/server.rb +3 -1
  61. data/lib/kward/plugin_registry.rb +12 -0
  62. data/lib/kward/private_file.rb +2 -0
  63. data/lib/kward/prompt_interface/banner.rb +3 -0
  64. data/lib/kward/prompt_interface/composer_controller.rb +262 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
  66. data/lib/kward/prompt_interface/composer_state.rb +221 -0
  67. data/lib/kward/prompt_interface/key_handler.rb +365 -0
  68. data/lib/kward/prompt_interface/layout.rb +31 -0
  69. data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
  70. data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
  71. data/lib/kward/prompt_interface/question_prompt.rb +328 -0
  72. data/lib/kward/prompt_interface/runtime_state.rb +59 -0
  73. data/lib/kward/prompt_interface/screen.rb +186 -0
  74. data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
  75. data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
  76. data/lib/kward/prompt_interface/stream_state.rb +65 -0
  77. data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
  78. data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
  79. data/lib/kward/prompt_interface.rb +69 -1832
  80. data/lib/kward/prompts/commands.rb +2 -0
  81. data/lib/kward/prompts/templates.rb +3 -0
  82. data/lib/kward/prompts.rb +2 -0
  83. data/lib/kward/question_contract.rb +66 -0
  84. data/lib/kward/resources/avatar_kward_logo.rb +2 -0
  85. data/lib/kward/resources/pixel_logo.rb +2 -0
  86. data/lib/kward/rpc/attachment_normalizer.rb +60 -0
  87. data/lib/kward/rpc/auth_manager.rb +65 -11
  88. data/lib/kward/rpc/config_manager.rb +11 -0
  89. data/lib/kward/rpc/prompt_bridge.rb +5 -26
  90. data/lib/kward/rpc/redactor.rb +3 -0
  91. data/lib/kward/rpc/runtime_payloads.rb +4 -1
  92. data/lib/kward/rpc/server.rb +37 -10
  93. data/lib/kward/rpc/session_manager.rb +123 -347
  94. data/lib/kward/rpc/session_metrics.rb +68 -0
  95. data/lib/kward/rpc/session_tree.rb +48 -0
  96. data/lib/kward/rpc/session_tree_rows.rb +208 -0
  97. data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
  98. data/lib/kward/rpc/tool_metadata.rb +3 -0
  99. data/lib/kward/rpc/transcript_normalizer.rb +3 -0
  100. data/lib/kward/rpc/transport.rb +3 -0
  101. data/lib/kward/session_diff.rb +2 -0
  102. data/lib/kward/session_store.rb +125 -31
  103. data/lib/kward/session_trash.rb +1 -0
  104. data/lib/kward/session_tree_renderer.rb +8 -41
  105. data/lib/kward/session_tree_tool_display.rb +56 -0
  106. data/lib/kward/skills/registry.rb +3 -0
  107. data/lib/kward/starter_pack_installer.rb +1 -0
  108. data/lib/kward/steering.rb +2 -0
  109. data/lib/kward/telemetry/logger.rb +3 -0
  110. data/lib/kward/telemetry/stats.rb +3 -0
  111. data/lib/kward/tools/ask_user_question.rb +20 -32
  112. data/lib/kward/tools/base.rb +8 -0
  113. data/lib/kward/tools/code_search.rb +5 -0
  114. data/lib/kward/tools/edit_file.rb +5 -0
  115. data/lib/kward/tools/list_directory.rb +5 -0
  116. data/lib/kward/tools/read_file.rb +5 -0
  117. data/lib/kward/tools/read_skill.rb +5 -0
  118. data/lib/kward/tools/registry.rb +33 -2
  119. data/lib/kward/tools/run_shell_command.rb +5 -0
  120. data/lib/kward/tools/search/code.rb +7 -0
  121. data/lib/kward/tools/search/web.rb +17 -14
  122. data/lib/kward/tools/tool_call.rb +25 -5
  123. data/lib/kward/tools/web_search.rb +7 -1
  124. data/lib/kward/tools/write_file.rb +5 -0
  125. data/lib/kward/transcript_export.rb +2 -0
  126. data/lib/kward/version.rb +2 -1
  127. data/lib/kward/workspace.rb +45 -5
  128. metadata +43 -1
@@ -0,0 +1,112 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Interactive slash-command parsing and dispatch helpers.
6
+ module SlashCommands
7
+ private
8
+
9
+ def handle_local_slash_command(command, agent, session_store)
10
+ name, argument = parse_slash_command(command)
11
+ case name
12
+ when "status"
13
+ run_busy_local_command_and_requeue { print_status }
14
+ [true, nil]
15
+ when "stats"
16
+ run_busy_local_command_and_requeue { print_stats(argument) }
17
+ [true, nil]
18
+ when "memory"
19
+ activity = memory_summarize_command?(argument) ? "summarizing" : "loading"
20
+ run_busy_local_command_and_requeue(activity: activity) { handle_memory_command(argument, agent) }
21
+ [true, nil]
22
+ when "redraw"
23
+ run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
24
+ [true, nil]
25
+ when "settings"
26
+ configure_settings(agent.conversation)
27
+ [true, nil]
28
+ when "login"
29
+ login_interactively
30
+ [true, nil]
31
+ when "model"
32
+ models = run_busy_local_command_and_requeue { normalized_available_models }
33
+ configure_model(agent.conversation, models: models)
34
+ [true, nil]
35
+ when "openrouter/catalog"
36
+ run_busy_local_command_and_requeue { print_openrouter_catalog }
37
+ [true, nil]
38
+ when "reasoning"
39
+ configure_reasoning(agent.conversation)
40
+ [true, nil]
41
+ when "reload"
42
+ run_busy_local_command_and_requeue { reload_plugins(agent.conversation) }
43
+ [true, nil]
44
+ when "new"
45
+ [true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
46
+ when "resume"
47
+ [true, run_busy_local_command_and_requeue do
48
+ path = argument.to_s.strip
49
+ path = select_session_path(session_store) if session_store && path.empty?
50
+ resume_session(session_store, path)
51
+ end]
52
+ when "name"
53
+ run_busy_local_command_and_requeue { rename_session(argument) }
54
+ [true, nil]
55
+ when "clone"
56
+ [true, run_busy_local_command_and_requeue { clone_session(session_store, agent) }]
57
+ when "tree"
58
+ [true, run_busy_local_command_and_requeue { navigate_session_tree(session_store) }]
59
+ when "copy"
60
+ run_busy_local_command_and_requeue { copy_session_text(agent.conversation, argument) }
61
+ [true, nil]
62
+ when "export"
63
+ run_busy_local_command_and_requeue { export_session(agent.conversation, argument) }
64
+ [true, nil]
65
+ when "compact"
66
+ run_busy_local_command_and_requeue(activity: "compacting") { compact_context(agent, argument) }
67
+ [true, nil]
68
+ else
69
+ return run_plugin_command(name, argument, agent) if plugin_command_for(name)
70
+
71
+ [false, nil]
72
+ end
73
+ end
74
+
75
+ def parse_slash_command(command)
76
+ PromptCommands.parse(command) || [nil, ""]
77
+ end
78
+
79
+ # Writes the status output for the terminal CLI flow.
80
+ def print_status
81
+ lines = [STATUS_MESSAGE]
82
+ lines << ""
83
+ lines << auto_compaction_status_line
84
+ if @active_session
85
+ lines << "Session: #{@active_session.name || @active_session.id}"
86
+ lines << "File: #{@active_session.path}"
87
+ end
88
+ lines.compact!
89
+ @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{lines.join("\n")}\n")
90
+ end
91
+
92
+ def auto_compaction_status_line
93
+ settings = Kward::Compaction::Settings.from_config
94
+ return "Auto-compaction: disabled" unless settings.enabled
95
+
96
+ context_window = composer_context_window
97
+ return "Auto-compaction: enabled, unknown context window" unless context_window.to_i.positive?
98
+
99
+ reserve_tokens = Kward::Compactor.auto_compaction_reserve_tokens(
100
+ context_window: context_window,
101
+ configured_reserve_tokens: settings.reserve_tokens
102
+ )
103
+ percent = ((reserve_tokens.to_f / context_window.to_i) * 100).round(1)
104
+ "Auto-compaction reserve: #{reserve_tokens} tokens (#{percent}% of #{context_window})"
105
+ rescue StandardError => e
106
+ warn "Auto-compaction status unavailable: #{e.message}"
107
+ nil
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,64 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Token statistics command helpers mixed into the CLI frontend.
6
+ module Stats
7
+ private
8
+
9
+ def export_token_stats(arguments)
10
+ options = parse_token_stats_options(arguments)
11
+ csv = TelemetryStats.new.token_usage_csv(options[:range], bucket: options[:bucket])
12
+ if options[:output]
13
+ File.write(options[:output], csv)
14
+ else
15
+ $stdout.write(csv)
16
+ end
17
+ rescue ArgumentError => e
18
+ warn e.message
19
+ warn "Usage: kward stats tokens [range] [--bucket second|minute|hour|day|week|month|year] [--output path]"
20
+ exit 1
21
+ end
22
+
23
+ def parse_token_stats_options(arguments)
24
+ remaining = []
25
+ bucket = nil
26
+ output = nil
27
+ index = 0
28
+ while index < arguments.length
29
+ argument = arguments[index]
30
+ case argument
31
+ when "--bucket"
32
+ index += 1
33
+ raise ArgumentError, "Missing value for --bucket" if index >= arguments.length
34
+
35
+ bucket = arguments[index]
36
+ when /\A--bucket=(.+)\z/
37
+ bucket = Regexp.last_match(1)
38
+ when "--output"
39
+ index += 1
40
+ raise ArgumentError, "Missing value for --output" if index >= arguments.length
41
+
42
+ output = arguments[index]
43
+ when /\A--output=(.+)\z/
44
+ output = Regexp.last_match(1)
45
+ else
46
+ remaining << argument
47
+ end
48
+ index += 1
49
+ end
50
+ { range: remaining.join(" "), bucket: bucket, output: output }
51
+ end
52
+
53
+ # Writes the stats output for the terminal CLI flow.
54
+ def print_stats(argument)
55
+ result = TelemetryStats.new.collect(argument)
56
+ @prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{TelemetryStats.format(result)}\n")
57
+ rescue ArgumentError => e
58
+ message = e.message == TelemetryStats::USAGE ? e.message : "#{e.message}\n#{TelemetryStats::USAGE}"
59
+ @prompt.say("\n#{message}\n")
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,153 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Compact tool-output summaries for terminal display and restored transcripts.
6
+ module ToolSummaries
7
+ private
8
+
9
+ def tool_result_summary(tool_call, content)
10
+ name = tool_call_name(tool_call)
11
+ args = tool_call_args(tool_call)
12
+ text = content.to_s
13
+ return error_tool_summary(name, args, text) if text.start_with?("Error:", "Declined:")
14
+
15
+ case name
16
+ when "read_file"
17
+ read_file_summary(args, text)
18
+ when "write_file", "edit_file"
19
+ file_change_summary(name, args, text)
20
+ when "run_shell_command"
21
+ shell_command_summary(args, text)
22
+ when "web_search"
23
+ web_search_summary(args, text)
24
+ else
25
+ generic_tool_summary(name, text)
26
+ end
27
+ end
28
+
29
+ def limit_tool_output_lines(content, line_limit)
30
+ lines = content.to_s.lines
31
+ return content.to_s if lines.length <= line_limit
32
+
33
+ kept_lines = lines.first(line_limit - 1).join
34
+ omitted_lines = lines.length - (line_limit - 1)
35
+ suffix = omitted_lines == 1 ? "line" : "lines"
36
+ notice = "...[truncated #{omitted_lines} #{suffix}]"
37
+ kept_lines.end_with?("\n") || kept_lines.empty? ? "#{kept_lines}#{notice}" : "#{kept_lines}\n#{notice}"
38
+ end
39
+
40
+ def read_file_summary(args, content)
41
+ path = args["path"] || args[:path] || "(unknown path)"
42
+ "read_file: #{path}\n#{content.lines.count} lines, #{content.bytesize} bytes"
43
+ end
44
+
45
+ def file_change_summary(name, args, content)
46
+ path = args["path"] || args[:path] || path_from_tool_result(content) || "(unknown path)"
47
+ concise = content.lines.first.to_s.strip
48
+ concise = "completed" if concise.empty?
49
+ "#{name}: #{path}\n#{concise}"
50
+ end
51
+
52
+ def shell_command_summary(args, content)
53
+ command = args["command"] || args[:command] || ""
54
+ lines = ["run_shell_command: #{command}".strip]
55
+ lines << "Exit status: #{shell_exit_status(content) || "unknown"}"
56
+ stdout = shell_section(content, "STDOUT")
57
+ stderr = shell_section(content, "STDERR")
58
+ lines << compact_stream_summary("stdout", stdout) unless stdout.empty?
59
+ lines << compact_stream_summary("stderr", stderr) unless stderr.empty?
60
+ lines.join("\n")
61
+ end
62
+
63
+ def web_search_summary(args, content)
64
+ queries = Array(args["queries"] || args[:queries]).map(&:to_s)
65
+ queries = web_search_queries_from_content(content) if queries.empty?
66
+ counts = web_search_result_counts(content)
67
+ lines = ["web_search"]
68
+ queries.each do |query|
69
+ lines << "#{query}: #{counts.fetch(query, 0)} result(s)"
70
+ end
71
+ lines << "#{web_search_total_count(content)} result(s)" if queries.empty?
72
+ lines.join("\n")
73
+ end
74
+
75
+ def error_tool_summary(name, args, content)
76
+ path = args["path"] || args[:path]
77
+ command = args["command"] || args[:command]
78
+ context = path || command
79
+ [name, context, content.lines.first.to_s.strip].compact.reject(&:empty?).join("\n")
80
+ end
81
+
82
+ def generic_tool_summary(name, content)
83
+ text = content.to_s
84
+ return "#{name}: #{text}" if text.length <= RESTORED_TOOL_OUTPUT_LIMIT
85
+
86
+ "#{name}: #{text[0, RESTORED_TOOL_OUTPUT_LIMIT]}\n...[truncated #{text.length - RESTORED_TOOL_OUTPUT_LIMIT} bytes]"
87
+ end
88
+
89
+ def compact_stream_summary(label, text)
90
+ summary = text.strip
91
+ summary = summary[0, 500] + "\n...[truncated #{summary.length - 500} chars]" if summary.length > 500
92
+ "#{label} (#{text.bytesize} bytes):#{summary.empty? ? "" : "\n#{summary}"}"
93
+ end
94
+
95
+ def shell_exit_status(content)
96
+ content.match(/^Exit status: ([^\n]+)/)&.[](1)
97
+ end
98
+
99
+ def shell_section(content, name)
100
+ match = content.match(/^#{Regexp.escape(name)}:\n(.*?)(?=\nSTD(?:OUT|ERR):\n|\z)/m)
101
+ match ? match[1] : ""
102
+ end
103
+
104
+ def web_search_queries_from_content(content)
105
+ content.scan(/^## Query: (.+)$/).flatten
106
+ end
107
+
108
+ def web_search_result_counts(content)
109
+ counts = {}
110
+ current_query = nil
111
+ content.each_line do |line|
112
+ if (match = line.match(/^## Query: (.+)$/))
113
+ current_query = match[1]
114
+ counts[current_query] ||= 0
115
+ elsif current_query && line.match?(/^\d+\. /)
116
+ counts[current_query] += 1
117
+ end
118
+ end
119
+ counts
120
+ end
121
+
122
+ def web_search_total_count(content)
123
+ content.each_line.count { |line| line.match?(/^\d+\. /) }
124
+ end
125
+
126
+ def path_from_tool_result(content)
127
+ content.match(/\b(?:to|file|Edited)\s+([^:\n]+?)(?:\s|:|\z)/)&.[](1)
128
+ end
129
+
130
+ def tool_call_name(tool_call)
131
+ ToolCall.name(tool_call) || "unknown_tool"
132
+ end
133
+
134
+ def tool_call_args(tool_call)
135
+ ToolCall.arguments(tool_call)
136
+ end
137
+
138
+ def tool_command(tool_call)
139
+ name = tool_call_name(tool_call)
140
+ args = tool_call_args(tool_call)
141
+
142
+ if name == "run_shell_command"
143
+ args["command"] || args[:command] || ""
144
+ elsif args.empty?
145
+ name.to_s
146
+ else
147
+ "#{name} #{JSON.dump(args)}"
148
+ end
149
+ end
150
+
151
+ end
152
+ end
153
+ end