kward 0.70.0 → 0.71.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +1 -1
  3. data/CHANGELOG.md +48 -2
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +90 -2
  6. data/README.md +30 -6
  7. data/Rakefile +96 -0
  8. data/doc/agent-tools.md +43 -0
  9. data/doc/api.md +92 -0
  10. data/doc/authentication.md +39 -25
  11. data/doc/configuration.md +1 -15
  12. data/doc/context-tools.md +70 -0
  13. data/doc/plugins.md +2 -2
  14. data/doc/releasing.md +14 -5
  15. data/doc/rpc.md +3 -11
  16. data/doc/session-management.md +220 -0
  17. data/doc/usage.md +7 -8
  18. data/doc/workspace-tools.md +105 -0
  19. data/lib/kward/cli/commands.rb +8 -0
  20. data/lib/kward/cli/openrouter_commands.rb +55 -0
  21. data/lib/kward/cli/prompt_interface.rb +80 -6
  22. data/lib/kward/cli/rendering.rb +11 -6
  23. data/lib/kward/cli/sessions.rb +260 -11
  24. data/lib/kward/cli/settings.rb +0 -30
  25. data/lib/kward/cli/slash_commands.rb +24 -6
  26. data/lib/kward/cli.rb +13 -0
  27. data/lib/kward/compactor.rb +4 -1
  28. data/lib/kward/config_files.rb +4 -6
  29. data/lib/kward/conversation.rb +49 -20
  30. data/lib/kward/model/client.rb +37 -50
  31. data/lib/kward/model/context_usage.rb +13 -6
  32. data/lib/kward/model/model_info.rb +92 -16
  33. data/lib/kward/model/payloads.rb +2 -0
  34. data/lib/kward/openrouter_model_cache.rb +120 -0
  35. data/lib/kward/plugin_registry.rb +47 -1
  36. data/lib/kward/prompt_interface/banner.rb +16 -51
  37. data/lib/kward/prompt_interface/composer_controller.rb +60 -87
  38. data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
  39. data/lib/kward/prompt_interface/key_handler.rb +31 -10
  40. data/lib/kward/prompt_interface/layout.rb +2 -2
  41. data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
  42. data/lib/kward/prompt_interface/question_prompt.rb +34 -42
  43. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  44. data/lib/kward/prompt_interface/screen.rb +1 -0
  45. data/lib/kward/prompt_interface/selection_prompt.rb +513 -54
  46. data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
  47. data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
  48. data/lib/kward/prompt_interface.rb +22 -28
  49. data/lib/kward/prompts/commands.rb +2 -1
  50. data/lib/kward/prompts.rb +2 -2
  51. data/lib/kward/rpc/server.rb +3 -8
  52. data/lib/kward/rpc/session_manager.rb +17 -6
  53. data/lib/kward/session_store.rb +23 -4
  54. data/lib/kward/telemetry/logger.rb +5 -3
  55. data/lib/kward/tool_output_compactor.rb +127 -0
  56. data/lib/kward/tools/base.rb +8 -2
  57. data/lib/kward/tools/registry.rb +37 -6
  58. data/lib/kward/tools/retrieve_tool_output.rb +71 -0
  59. data/lib/kward/tools/search/web.rb +2 -2
  60. data/lib/kward/tools/summarize_file_structure.rb +29 -0
  61. data/lib/kward/tools/tool_call.rb +2 -0
  62. data/lib/kward/version.rb +1 -1
  63. data/lib/kward/workspace.rb +58 -2
  64. data/templates/default/fulldoc/html/css/kward.css +256 -7
  65. data/templates/default/fulldoc/html/full_list.erb +107 -0
  66. data/templates/default/fulldoc/html/js/kward.js +161 -2
  67. data/templates/default/fulldoc/html/setup.rb +8 -0
  68. data/templates/default/kward_navigation.rb +91 -0
  69. data/templates/default/layout/html/layout.erb +39 -8
  70. data/templates/default/layout/html/setup.rb +33 -38
  71. metadata +13 -3
  72. data/lib/kward/resources/avatar_kward_logo.rb +0 -50
  73. data/lib/kward/resources/pixel_logo.rb +0 -232
@@ -1,232 +0,0 @@
1
- require "zlib"
2
-
3
- # Namespace for the Kward CLI agent runtime.
4
- module Kward
5
- # Pixel-art logo data and rendering helpers.
6
- module PixelLogo
7
- PNG_SIGNATURE = "\x89PNG\r\n\x1a\n".b.freeze
8
- TRANSPARENT_ALPHA = 128
9
-
10
- module_function
11
-
12
- def rows_from_png(path, width:, height:)
13
- rows_from_pixels(indexed_png_pixels(path), width: width, height: height)
14
- rescue StandardError
15
- []
16
- end
17
-
18
- def rows_from_pixels(pixels, width:, height:)
19
- scaled = scale_pixels(pixels, width: width, height: height)
20
- render_rows(scaled)
21
- rescue StandardError
22
- []
23
- end
24
-
25
- def half_block_rows_from_png(path, width:, pixel_height:)
26
- half_block_rows_from_pixels(indexed_png_pixels(path), width: width, pixel_height: pixel_height)
27
- rescue StandardError
28
- []
29
- end
30
-
31
- def half_block_rows_from_pixels(pixels, width:, pixel_height:)
32
- scaled = scale_pixels(pixels, width: width, height: pixel_height)
33
- render_half_block_rows(scaled)
34
- rescue StandardError
35
- []
36
- end
37
-
38
- def indexed_png_pixels(path)
39
- data = File.binread(path)
40
- raise "Invalid PNG" unless data.start_with?(PNG_SIGNATURE)
41
-
42
- png_width = nil
43
- png_height = nil
44
- bit_depth = nil
45
- color_type = nil
46
- interlace = nil
47
- palette = nil
48
- transparency = []
49
- idat = +"".b
50
-
51
- each_chunk(data) do |type, chunk|
52
- case type
53
- when "IHDR"
54
- png_width, png_height, bit_depth, color_type, _compression, _filter, interlace = chunk.unpack("NNCCCCC")
55
- when "PLTE"
56
- palette = chunk.bytes.each_slice(3).map { |red, green, blue| [red, green, blue, 255] }
57
- when "tRNS"
58
- transparency = chunk.bytes
59
- when "IDAT"
60
- idat << chunk
61
- end
62
- end
63
-
64
- raise "Unsupported PNG" unless png_width && png_height && palette
65
- raise "Unsupported PNG" unless bit_depth == 8 && color_type == 3 && interlace == 0
66
-
67
- transparency.each_with_index do |alpha, index|
68
- palette[index] = palette[index][0, 3] + [alpha] if palette[index]
69
- end
70
-
71
- unfilter_indexed_rows(Zlib::Inflate.inflate(idat), png_width, png_height).map do |row|
72
- row.map { |index| palette[index] || [0, 0, 0, 0] }
73
- end
74
- end
75
-
76
- def each_chunk(data)
77
- offset = PNG_SIGNATURE.bytesize
78
- while offset < data.bytesize
79
- length = data[offset, 4].unpack1("N")
80
- type = data[offset + 4, 4]
81
- chunk = data[offset + 8, length]
82
- yield type, chunk
83
- offset += length + 12
84
- end
85
- end
86
-
87
- def unfilter_indexed_rows(raw, width, height)
88
- rows = []
89
- previous = Array.new(width, 0)
90
- offset = 0
91
-
92
- height.times do
93
- filter = raw.getbyte(offset)
94
- offset += 1
95
- scanline = raw.byteslice(offset, width).bytes
96
- offset += width
97
- row = unfilter_scanline(scanline, previous, filter)
98
- rows << row
99
- previous = row
100
- end
101
-
102
- rows
103
- end
104
-
105
- def unfilter_scanline(scanline, previous, filter)
106
- row = []
107
- scanline.each_with_index do |byte, index|
108
- left = index.zero? ? 0 : row[index - 1]
109
- up = previous[index] || 0
110
- up_left = index.zero? ? 0 : previous[index - 1]
111
- row << case filter
112
- when 0 then byte
113
- when 1 then (byte + left) & 0xff
114
- when 2 then (byte + up) & 0xff
115
- when 3 then (byte + ((left + up) / 2)) & 0xff
116
- when 4 then (byte + paeth(left, up, up_left)) & 0xff
117
- else byte
118
- end
119
- end
120
- row
121
- end
122
-
123
- def paeth(left, up, up_left)
124
- estimate = left + up - up_left
125
- left_distance = (estimate - left).abs
126
- up_distance = (estimate - up).abs
127
- up_left_distance = (estimate - up_left).abs
128
- return left if left_distance <= up_distance && left_distance <= up_left_distance
129
- return up if up_distance <= up_left_distance
130
-
131
- up_left
132
- end
133
-
134
- def scale_pixels(pixels, width:, height:)
135
- source_height = pixels.length
136
- source_width = pixels.first.length
137
-
138
- height.times.map do |target_y|
139
- source_y_start = target_y * source_height / height
140
- source_y_end = [(target_y + 1) * source_height / height, source_y_start + 1].max
141
- width.times.map do |target_x|
142
- source_x_start = target_x * source_width / width
143
- source_x_end = [(target_x + 1) * source_width / width, source_x_start + 1].max
144
- dominant_color(pixels, source_x_start...source_x_end, source_y_start...source_y_end)
145
- end
146
- end
147
- end
148
-
149
- def dominant_color(pixels, x_range, y_range)
150
- counts = Hash.new(0)
151
- y_range.each do |y|
152
- x_range.each do |x|
153
- counts[visible_color(pixels[y][x])] += 1
154
- end
155
- end
156
- counts.max_by { |_color, count| count }&.first
157
- end
158
-
159
- def visible_color(color)
160
- return nil unless color
161
-
162
- red, green, blue, alpha = color
163
- return nil if !alpha.nil? && alpha.to_i < TRANSPARENT_ALPHA
164
-
165
- [red, green, blue]
166
- end
167
-
168
- def render_rows(rows)
169
- rows.map do |row|
170
- current = nil
171
- rendered = +""
172
- row.each do |color|
173
- if color != current
174
- rendered << background_sgr(color)
175
- current = color
176
- end
177
- rendered << " "
178
- end
179
- rendered << reset_sgr if current
180
- rendered
181
- end
182
- end
183
-
184
- def render_half_block_rows(rows)
185
- rows.each_slice(2).map do |top_row, bottom_row|
186
- bottom_row ||= Array.new(top_row.length)
187
- current_foreground = nil
188
- current_background = nil
189
- rendered = +""
190
- top_row.each_with_index do |top, index|
191
- bottom = bottom_row[index]
192
- foreground, background, glyph = half_block_cell(top, bottom)
193
- if background != current_background
194
- rendered << background_sgr(background)
195
- current_background = background
196
- end
197
- if foreground != current_foreground
198
- rendered << foreground_sgr(foreground)
199
- current_foreground = foreground
200
- end
201
- rendered << glyph
202
- end
203
- rendered << reset_sgr if current_foreground || current_background
204
- rendered
205
- end
206
- end
207
-
208
- def half_block_cell(top, bottom)
209
- if top && bottom
210
- [top, bottom, "▀"]
211
- elsif top
212
- [top, nil, "▀"]
213
- elsif bottom
214
- [bottom, nil, "▄"]
215
- else
216
- [nil, nil, " "]
217
- end
218
- end
219
-
220
- def foreground_sgr(color)
221
- color ? "\e[38;2;#{color.join(";")}m" : "\e[39m"
222
- end
223
-
224
- def background_sgr(color)
225
- color ? "\e[48;2;#{color.join(";")}m" : "\e[49m"
226
- end
227
-
228
- def reset_sgr
229
- "\e[0m"
230
- end
231
- end
232
- end