kward 0.69.1 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +1 -1
  3. data/CHANGELOG.md +68 -0
  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 +2 -16
  12. data/doc/context-tools.md +70 -0
  13. data/doc/getting-started.md +3 -1
  14. data/doc/plugins.md +2 -2
  15. data/doc/releasing.md +14 -5
  16. data/doc/rpc.md +3 -11
  17. data/doc/session-management.md +220 -0
  18. data/doc/usage.md +13 -7
  19. data/doc/workspace-tools.md +105 -0
  20. data/lib/kward/cli/commands.rb +8 -0
  21. data/lib/kward/cli/openrouter_commands.rb +55 -0
  22. data/lib/kward/cli/prompt_interface.rb +85 -7
  23. data/lib/kward/cli/rendering.rb +11 -6
  24. data/lib/kward/cli/sessions.rb +454 -15
  25. data/lib/kward/cli/settings.rb +0 -30
  26. data/lib/kward/cli/slash_commands.rb +38 -11
  27. data/lib/kward/cli.rb +14 -0
  28. data/lib/kward/compactor.rb +4 -1
  29. data/lib/kward/config_files.rb +4 -6
  30. data/lib/kward/conversation.rb +49 -5
  31. data/lib/kward/model/client.rb +37 -50
  32. data/lib/kward/model/context_usage.rb +13 -6
  33. data/lib/kward/model/model_info.rb +92 -9
  34. data/lib/kward/model/payloads.rb +2 -0
  35. data/lib/kward/openrouter_model_cache.rb +120 -0
  36. data/lib/kward/plugin_registry.rb +47 -1
  37. data/lib/kward/prompt_interface/banner.rb +16 -51
  38. data/lib/kward/prompt_interface/composer_controller.rb +60 -87
  39. data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
  40. data/lib/kward/prompt_interface/key_handler.rb +31 -10
  41. data/lib/kward/prompt_interface/layout.rb +2 -2
  42. data/lib/kward/prompt_interface/overlay_renderer.rb +24 -0
  43. data/lib/kward/prompt_interface/prompt_renderer.rb +23 -2
  44. data/lib/kward/prompt_interface/question_prompt.rb +34 -42
  45. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  46. data/lib/kward/prompt_interface/screen.rb +10 -4
  47. data/lib/kward/prompt_interface/selection_prompt.rb +518 -61
  48. data/lib/kward/prompt_interface/slash_overlay.rb +4 -4
  49. data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
  50. data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
  51. data/lib/kward/prompt_interface.rb +31 -32
  52. data/lib/kward/prompts/commands.rb +6 -3
  53. data/lib/kward/prompts.rb +2 -2
  54. data/lib/kward/rpc/server.rb +3 -8
  55. data/lib/kward/rpc/session_manager.rb +19 -8
  56. data/lib/kward/session_diff.rb +106 -9
  57. data/lib/kward/session_store.rb +23 -4
  58. data/lib/kward/session_tree_renderer.rb +2 -1
  59. data/lib/kward/telemetry/logger.rb +5 -3
  60. data/lib/kward/tool_output_compactor.rb +127 -0
  61. data/lib/kward/tools/base.rb +8 -2
  62. data/lib/kward/tools/registry.rb +37 -6
  63. data/lib/kward/tools/retrieve_tool_output.rb +71 -0
  64. data/lib/kward/tools/search/web.rb +2 -2
  65. data/lib/kward/tools/summarize_file_structure.rb +29 -0
  66. data/lib/kward/tools/tool_call.rb +2 -0
  67. data/lib/kward/version.rb +1 -1
  68. data/lib/kward/workspace.rb +58 -2
  69. data/templates/default/fulldoc/html/css/kward.css +570 -78
  70. data/templates/default/fulldoc/html/full_list.erb +107 -0
  71. data/templates/default/fulldoc/html/js/kward.js +259 -97
  72. data/templates/default/fulldoc/html/setup.rb +8 -0
  73. data/templates/default/kward_navigation.rb +91 -0
  74. data/templates/default/layout/html/layout.erb +59 -13
  75. data/templates/default/layout/html/setup.rb +34 -39
  76. metadata +13 -3
  77. data/lib/kward/resources/avatar_kward_logo.rb +0 -50
  78. data/lib/kward/resources/pixel_logo.rb +0 -232
@@ -1,10 +1,10 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <%= erb(:headers) %>
5
5
  </head>
6
6
  <body class="kward-docs <%= home_page? ? 'kward-home-body' : 'kward-content-body' %>">
7
- <div id="main_progress" aria-hidden="true"></div>
7
+ <a href="#main" class="kward-skip-link">Skip to content</a>
8
8
 
9
9
  <header class="kward-topbar">
10
10
  <a class="kward-brand" href="<%= url_for('index.html') %>">
@@ -13,14 +13,17 @@
13
13
  <strong>Kward</strong>
14
14
  </span>
15
15
  </a>
16
- <button class="kward-nav-toggle" type="button" aria-expanded="false" aria-controls="kward-primary-nav" onclick="document.body.classList.toggle('kward-nav-open'); this.setAttribute('aria-expanded', document.body.classList.contains('kward-nav-open'))">
17
- Menu
16
+ <button class="kward-nav-toggle" type="button" aria-expanded="false" aria-controls="kward-primary-nav" aria-label="Toggle navigation menu" onclick="document.body.classList.toggle('kward-nav-open'); this.setAttribute('aria-expanded', document.body.classList.contains('kward-nav-open'))">
17
+ <span class="kward-nav-toggle-bar"></span>
18
+ <span class="kward-nav-toggle-bar"></span>
19
+ <span class="kward-nav-toggle-bar"></span>
20
+ <span class="kward-sr-only">Menu</span>
18
21
  </button>
19
22
  <nav id="kward-primary-nav" class="kward-topnav" aria-label="Primary navigation">
20
23
  <a href="<%= url_for('index.html') %>" class="<%= 'active' if home_page? %>">Home</a>
21
24
  <div class="kward-nav-menu <%= 'active' if guide_page? %>">
22
- <a class="kward-nav-menu-link" href="<%= url_for('file.README.html') %>">User Guides</a>
23
- <button class="kward-nav-menu-button" type="button" aria-label="Open user guides menu">⌄</button>
25
+ <a class="kward-nav-menu-link" href="<%= url_for(guide_overview) %>">Guides</a>
26
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand guides" aria-expanded="false">⌄</button>
24
27
  <div class="kward-nav-dropdown">
25
28
  <% guide_groups.each do |title, items| %>
26
29
  <section>
@@ -32,13 +35,44 @@
32
35
  <% end %>
33
36
  </div>
34
37
  </div>
35
- <a href="<%= url_for('Kward.html') %>" class="<%= 'active' if api_page? %>">API Docs</a>
38
+ <div class="kward-nav-menu <%= 'active' if extension_page? %>">
39
+ <a class="kward-nav-menu-link" href="<%= url_for(extension_overview) %>">Advanced</a>
40
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand advanced guides" aria-expanded="false">⌄</button>
41
+ <div class="kward-nav-dropdown kward-advanced-nav-dropdown">
42
+ <% extension_groups.each do |title, items| %>
43
+ <section>
44
+ <h2><%= h title %></h2>
45
+ <% items.each do |label, link| %>
46
+ <a href="<%= url_for(link) %>"><%= h label %></a>
47
+ <% end %>
48
+ </section>
49
+ <% end %>
50
+ </div>
51
+ </div>
52
+ <div class="kward-nav-menu <%= 'active' if api_page? %>">
53
+ <a class="kward-nav-menu-link" href="<%= url_for(api_overview) %>">API Docs</a>
54
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand API docs" aria-expanded="false">⌄</button>
55
+ <div class="kward-nav-dropdown kward-api-nav-dropdown">
56
+ <% api_groups.each do |title, items| %>
57
+ <section>
58
+ <h2><%= h title %></h2>
59
+ <% items.each do |label, link| %>
60
+ <a href="<%= url_for(link) %>"><%= h label %></a>
61
+ <% end %>
62
+ </section>
63
+ <% end %>
64
+ </div>
65
+ </div>
36
66
  </nav>
37
- <form class="kward-guide-search" role="search">
38
- <label class="kward-sr-only" for="kward-guide-search-input">Search user guides</label>
39
- <input id="kward-guide-search-input" type="search" placeholder="Search guides" autocomplete="off" aria-label="Search user guides" aria-controls="kward-guide-search-results">
40
- <div id="kward-guide-search-results" class="kward-guide-search-results" role="listbox" aria-label="Search results"></div>
41
- </form>
67
+ <% if api_page? %>
68
+ <a class="kward-api-index-link" href="<%= url_for('class_list.html') %>">Search API index</a>
69
+ <% else %>
70
+ <form class="kward-guide-search" role="search">
71
+ <label class="kward-sr-only" for="kward-guide-search-input">Search user guides</label>
72
+ <input id="kward-guide-search-input" type="search" placeholder="Search guides" autocomplete="off" aria-label="Search user guides" aria-controls="kward-guide-search-results">
73
+ <div id="kward-guide-search-results" class="kward-guide-search-results" role="listbox" aria-label="Search results"></div>
74
+ </form>
75
+ <% end %>
42
76
  <script type="application/json" id="kward-guide-search-index"><%= guide_search_index_json %></script>
43
77
  </header>
44
78
 
@@ -47,7 +81,17 @@
47
81
  <section class="kward-hero">
48
82
  <div class="kward-hero-copy">
49
83
  <p class="kward-eyebrow">⌘ Ruby CLI Coding Agent</p>
50
- <h1>Your terminal.<br><span>Your agent.</span></h1>
84
+ <h1>
85
+ <span class="kward-swosh" aria-label="Your terminal.">
86
+ <span class="kward-swosh-text kward-swosh-text-a" aria-hidden="true">Your terminal.</span>
87
+ <span class="kward-swosh-text kward-swosh-text-b" aria-hidden="true">Your RPC frontend.</span>
88
+ </span>
89
+ <br>
90
+ <span class="kward-swosh" aria-label="Your agent.">
91
+ <span class="kward-swosh-text kward-swosh-text-c" aria-hidden="true">Your agent.</span>
92
+ <span class="kward-swosh-text kward-swosh-text-d" aria-hidden="true">Your LLM engine.</span>
93
+ </span>
94
+ </h1>
51
95
  <p class="kward-lede">Kward is an extendable Ruby CLI coding agent that helps you understand your project, edit files, run commands, search the web, and automate workflows—right from your terminal.</p>
52
96
  <div class="kward-actions">
53
97
  <a class="kward-primary-button" href="<%= url_for('file.README.html') %>">Get Started ›</a>
@@ -137,5 +181,7 @@ kward --working-directory ~/code/project "Explain this project"</code></pre>
137
181
  </main>
138
182
  </div>
139
183
  <% end %>
184
+
185
+ <script type="text/javascript" charset="utf-8" src="<%= url_for('js/kward.js') %>"></script>
140
186
  </body>
141
187
  </html>
@@ -1,44 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../../kward_navigation"
4
5
 
5
6
  module KwardDocsNavigation
6
- GUIDE_GROUPS = [
7
- [
8
- "Start here",
9
- [
10
- ["Getting started", "file.getting-started.html"],
11
- ["Usage", "file.usage.html"],
12
- ["Configuration", "file.configuration.html"],
13
- ["Authentication", "file.authentication.html"],
14
- ["Troubleshooting", "file.troubleshooting.html"]
15
- ]
16
- ],
17
- [
18
- "Feature guides",
19
- [
20
- ["Memory", "file.memory.html"],
21
- ["Personas", "file.personas.html"],
22
- ["Extensibility", "file.extensibility.html"],
23
- ["Plugins", "file.plugins.html"],
24
- ["Web search", "file.web-search.html"],
25
- ["Code search", "file.code-search.html"]
26
- ]
27
- ],
28
- [
29
- "Advanced/reference",
30
- [
31
- ["RPC protocol", "file.rpc.html"],
32
- ["Releasing", "file.releasing.html"]
33
- ]
34
- ]
35
- ].freeze
7
+ include KwardDocsNavigationData
8
+ API_FILE_LINKS = {
9
+ "doc/api.md" => API_OVERVIEW
10
+ }.freeze
11
+ API_LINKS = ([API_OVERVIEW] + API_GROUPS.flat_map { |_title, items| items.map(&:last) }).freeze
12
+
13
+ EXTENSION_OVERVIEW = "file.extensibility.html"
14
+ EXTENSION_LINKS = ([EXTENSION_OVERVIEW] + EXTENSION_GROUPS.flat_map { |_title, items| items.map(&:last) }).uniq.freeze
36
15
 
37
16
  GUIDE_OVERVIEW = "file.README.html"
38
17
  GUIDE_LINKS = ([GUIDE_OVERVIEW] + GUIDE_GROUPS.flat_map { |_title, items| items.map(&:last) }).freeze
39
18
  GUIDE_SEARCH_FILES = [
40
19
  ["Overview", "README.md", GUIDE_OVERVIEW],
41
- *GUIDE_GROUPS.flat_map do |_title, items|
20
+ *(GUIDE_GROUPS + EXTENSION_GROUPS).flat_map do |_title, items|
42
21
  items.map do |label, link|
43
22
  source = "doc/#{link.delete_prefix("file.").delete_suffix(".html")}.md"
44
23
  [label, source, link]
@@ -51,24 +30,28 @@ module KwardDocsNavigation
51
30
  "doc/configuration.md" => "file.configuration.html",
52
31
  "doc/authentication.md" => "file.authentication.html",
53
32
  "doc/troubleshooting.md" => "file.troubleshooting.html",
33
+ "doc/session-management.md" => "file.session-management.html",
54
34
  "doc/memory.md" => "file.memory.html",
55
35
  "doc/personas.md" => "file.personas.html",
56
36
  "doc/extensibility.md" => "file.extensibility.html",
57
37
  "doc/plugins.md" => "file.plugins.html",
38
+ "doc/agent-tools.md" => "file.agent-tools.html",
39
+ "doc/workspace-tools.md" => "file.workspace-tools.html",
58
40
  "doc/web-search.md" => "file.web-search.html",
59
41
  "doc/code-search.md" => "file.code-search.html",
42
+ "doc/context-tools.md" => "file.context-tools.html",
60
43
  "doc/rpc.md" => "file.rpc.html",
61
44
  "doc/releasing.md" => "file.releasing.html"
62
45
  }.freeze
63
46
 
64
- def guide_groups
65
- GUIDE_GROUPS
66
- end
67
-
68
47
  def guide_overview
69
48
  GUIDE_OVERVIEW
70
49
  end
71
50
 
51
+ def extension_overview
52
+ EXTENSION_OVERVIEW
53
+ end
54
+
72
55
  def guide_search_index_json
73
56
  JSON.generate(guide_search_index).gsub("</", "<\\/")
74
57
  end
@@ -99,7 +82,15 @@ module KwardDocsNavigation
99
82
  end
100
83
 
101
84
  def guide_file?
102
- defined?(@file) && GUIDE_FILE_LINKS.key?(@file&.filename.to_s)
85
+ defined?(@file) && GUIDE_LINKS.include?(GUIDE_FILE_LINKS[@file&.filename.to_s])
86
+ end
87
+
88
+ def extension_file?
89
+ defined?(@file) && EXTENSION_LINKS.include?(GUIDE_FILE_LINKS[@file&.filename.to_s])
90
+ end
91
+
92
+ def api_file?
93
+ defined?(@file) && API_FILE_LINKS.key?(@file&.filename.to_s)
103
94
  end
104
95
 
105
96
  def home_page?
@@ -110,8 +101,12 @@ module KwardDocsNavigation
110
101
  (readme_file? && !options.index) || guide_file? || GUIDE_LINKS.include?(current_docs_path)
111
102
  end
112
103
 
104
+ def extension_page?
105
+ extension_file? || EXTENSION_LINKS.include?(current_docs_path)
106
+ end
107
+
113
108
  def api_page?
114
- !home_page? && !guide_page?
109
+ api_file? || API_LINKS.include?(current_docs_path) || (!home_page? && !guide_page? && !extension_page?)
115
110
  end
116
111
 
117
112
  def diskfile
@@ -121,7 +116,7 @@ module KwardDocsNavigation
121
116
  end
122
117
 
123
118
  def rewrite_guide_links(html)
124
- GUIDE_FILE_LINKS.each do |source, target|
119
+ GUIDE_FILE_LINKS.merge(API_FILE_LINKS).each do |source, target|
125
120
  html = html.gsub(%(href="#{source}"), %(href="#{target}"))
126
121
  end
127
122
  html
@@ -131,7 +126,7 @@ end
131
126
  include KwardDocsNavigation
132
127
 
133
128
  def javascripts
134
- super + %w(js/kward.js)
129
+ super
135
130
  end
136
131
 
137
132
  def stylesheets
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kward
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.69.1
4
+ version: 0.71.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Wood
@@ -124,9 +124,12 @@ files:
124
124
  - LICENSE
125
125
  - README.md
126
126
  - Rakefile
127
+ - doc/agent-tools.md
128
+ - doc/api.md
127
129
  - doc/authentication.md
128
130
  - doc/code-search.md
129
131
  - doc/configuration.md
132
+ - doc/context-tools.md
130
133
  - doc/extensibility.md
131
134
  - doc/getting-started.md
132
135
  - doc/memory.md
@@ -134,9 +137,11 @@ files:
134
137
  - doc/plugins.md
135
138
  - doc/releasing.md
136
139
  - doc/rpc.md
140
+ - doc/session-management.md
137
141
  - doc/troubleshooting.md
138
142
  - doc/usage.md
139
143
  - doc/web-search.md
144
+ - doc/workspace-tools.md
140
145
  - exe/kward
141
146
  - kward.gemspec
142
147
  - lib/kward.rb
@@ -155,6 +160,7 @@ files:
155
160
  - lib/kward/cli/doctor.rb
156
161
  - lib/kward/cli/interactive_turn.rb
157
162
  - lib/kward/cli/memory_commands.rb
163
+ - lib/kward/cli/openrouter_commands.rb
158
164
  - lib/kward/cli/plugins.rb
159
165
  - lib/kward/cli/prompt_interface.rb
160
166
  - lib/kward/cli/rendering.rb
@@ -186,6 +192,7 @@ files:
186
192
  - lib/kward/model/payloads.rb
187
193
  - lib/kward/model/retry_message.rb
188
194
  - lib/kward/model/stream_parser.rb
195
+ - lib/kward/openrouter_model_cache.rb
189
196
  - lib/kward/pan/index.html.erb
190
197
  - lib/kward/pan/server.rb
191
198
  - lib/kward/plugin_registry.rb
@@ -211,8 +218,6 @@ files:
211
218
  - lib/kward/prompts/commands.rb
212
219
  - lib/kward/prompts/templates.rb
213
220
  - lib/kward/question_contract.rb
214
- - lib/kward/resources/avatar_kward_logo.rb
215
- - lib/kward/resources/pixel_logo.rb
216
221
  - lib/kward/rpc/attachment_normalizer.rb
217
222
  - lib/kward/rpc/auth_manager.rb
218
223
  - lib/kward/rpc/config_manager.rb
@@ -238,6 +243,7 @@ files:
238
243
  - lib/kward/steering.rb
239
244
  - lib/kward/telemetry/logger.rb
240
245
  - lib/kward/telemetry/stats.rb
246
+ - lib/kward/tool_output_compactor.rb
241
247
  - lib/kward/tools/ask_user_question.rb
242
248
  - lib/kward/tools/base.rb
243
249
  - lib/kward/tools/code_search.rb
@@ -248,10 +254,12 @@ files:
248
254
  - lib/kward/tools/read_file.rb
249
255
  - lib/kward/tools/read_skill.rb
250
256
  - lib/kward/tools/registry.rb
257
+ - lib/kward/tools/retrieve_tool_output.rb
251
258
  - lib/kward/tools/run_shell_command.rb
252
259
  - lib/kward/tools/search/code.rb
253
260
  - lib/kward/tools/search/web.rb
254
261
  - lib/kward/tools/search/web_fetch.rb
262
+ - lib/kward/tools/summarize_file_structure.rb
255
263
  - lib/kward/tools/tool_call.rb
256
264
  - lib/kward/tools/web_search.rb
257
265
  - lib/kward/tools/write_file.rb
@@ -260,9 +268,11 @@ files:
260
268
  - lib/kward/workspace.rb
261
269
  - lib/main.rb
262
270
  - templates/default/fulldoc/html/css/kward.css
271
+ - templates/default/fulldoc/html/full_list.erb
263
272
  - templates/default/fulldoc/html/images/kward_logo.png
264
273
  - templates/default/fulldoc/html/js/kward.js
265
274
  - templates/default/fulldoc/html/setup.rb
275
+ - templates/default/kward_navigation.rb
266
276
  - templates/default/layout/html/breadcrumb.erb
267
277
  - templates/default/layout/html/layout.erb
268
278
  - templates/default/layout/html/setup.rb
@@ -1,50 +0,0 @@
1
- # Generated from avatar_kward_32x32.png as RGB terminal cells.
2
- # The interactive banner uses this data instead of decoding a PNG at runtime.
3
- module Kward
4
- # Static avatar logo data used by generated resources.
5
- module Resources
6
- # Static avatar logo data used by generated resources.
7
- module AvatarKwardLogo
8
- PIXELS = [
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],
10
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [36, 40, 24], [39, 42, 25], [39, 42, 25], [33, 36, 21], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
11
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [33, 36, 23], [78, 86, 52], [85, 93, 56], [84, 93, 55], [65, 72, 43], [31, 33, 19], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
12
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [49, 54, 33], [78, 86, 52], [85, 93, 56], [85, 93, 56], [85, 93, 56], [84, 93, 55], [75, 83, 49], [35, 38, 23], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
13
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [61, 68, 40], [80, 88, 53], [85, 93, 56], [75, 83, 49], [72, 80, 48], [70, 78, 46], [78, 86, 52], [84, 93, 55], [76, 85, 51], [50, 55, 34], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
14
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [42, 46, 27], [80, 88, 53], [84, 93, 55], [70, 78, 46], [23, 25, 15], [37, 38, 29], [34, 35, 26], [33, 37, 22], [74, 81, 48], [83, 92, 55], [78, 86, 52], [24, 26, 15], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
15
- [nil, nil, nil, nil, nil, nil, nil, nil, nil, [34, 37, 22], [83, 92, 55], [85, 93, 56], [69, 77, 46], [18, 19, 12], [53, 53, 45], [135, 132, 115], [128, 125, 111], [40, 40, 36], [21, 23, 14], [72, 80, 48], [80, 88, 53], [39, 44, 26], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil],
16
- [nil, nil, nil, nil, nil, nil, nil, nil, [18, 20, 13], [58, 63, 38], [85, 93, 56], [55, 61, 36], [23, 25, 15], [43, 42, 35], [146, 142, 129], [176, 173, 156], [179, 175, 159], [146, 142, 129], [67, 65, 58], [27, 30, 19], [62, 68, 41], [80, 88, 53], [54, 60, 35], nil, nil, nil, nil, nil, nil, nil, nil, nil],
17
- [nil, nil, nil, nil, nil, nil, nil, nil, [23, 26, 16], [74, 81, 48], [69, 77, 46], [21, 23, 14], [31, 31, 25], [108, 105, 91], [153, 150, 134], [134, 132, 118], [173, 169, 154], [183, 179, 163], [134, 132, 118], [27, 26, 21], [25, 28, 16], [70, 78, 46], [72, 80, 48], [21, 23, 14], nil, nil, nil, nil, nil, nil, nil, nil],
18
- [nil, nil, nil, nil, nil, nil, nil, [8, 10, 6], [66, 73, 45], [58, 63, 37], [14, 15, 9], [3, 3, 2], [87, 85, 74], [160, 157, 139], [160, 157, 139], [134, 132, 118], [80, 78, 69], [173, 169, 154], [175, 171, 155], [76, 74, 65], [5, 5, 4], [14, 15, 9], [63, 70, 42], [66, 73, 45], [2, 3, 2], nil, nil, nil, nil, nil, nil, nil],
19
- [nil, nil, nil, nil, nil, nil, nil, [18, 19, 12], [59, 65, 40], [23, 23, 21], [0, 0, 0], [100, 98, 86], [160, 157, 139], [163, 160, 143], [138, 136, 121], [70, 68, 58], [173, 169, 154], [175, 171, 155], [183, 179, 163], [179, 175, 159], [92, 90, 82], [1, 1, 1], [27, 30, 19], [62, 68, 41], [16, 17, 11], nil, nil, nil, nil, nil, nil, nil],
20
- [nil, nil, nil, nil, nil, nil, [15, 15, 11], [62, 68, 42], [72, 70, 61], [87, 85, 74], [44, 43, 36], [77, 76, 65], [108, 105, 91], [149, 146, 130], [123, 119, 105], [158, 155, 140], [168, 164, 149], [144, 140, 126], [160, 156, 142], [118, 115, 104], [77, 76, 65], [53, 53, 45], [103, 102, 85], [76, 74, 65], [66, 72, 43], [20, 20, 13], nil, nil, nil, nil, nil, nil],
21
- [nil, nil, nil, nil, nil, nil, [35, 38, 23], [78, 86, 52], [135, 132, 115], [113, 109, 94], [129, 128, 109], [23, 25, 15], [33, 55, 8], [96, 108, 72], [87, 85, 74], [168, 164, 149], [158, 155, 140], [97, 94, 83], [96, 108, 72], [24, 40, 6], [30, 33, 20], [142, 141, 122], [104, 102, 90], [153, 150, 134], [75, 83, 49], [37, 41, 25], nil, nil, nil, nil, nil, nil],
22
- [nil, nil, nil, nil, nil, [2, 2, 1], [63, 70, 42], [87, 95, 60], [134, 132, 118], [77, 76, 65], [104, 102, 90], [5, 5, 4], [72, 133, 6], [100, 184, 10], [39, 44, 26], [160, 156, 142], [146, 142, 129], [47, 70, 19], [103, 188, 10], [56, 105, 5], [10, 10, 8], [128, 125, 111], [72, 70, 61], [149, 146, 130], [84, 93, 55], [54, 60, 36], [2, 3, 2], nil, nil, nil, nil, nil],
23
- [nil, nil, nil, nil, nil, [46, 51, 30], [67, 74, 44], [44, 48, 30], [114, 111, 97], [80, 78, 69], [103, 102, 85], [34, 35, 26], [33, 55, 8], [59, 97, 13], [82, 84, 65], [166, 162, 147], [156, 152, 138], [76, 85, 51], [59, 97, 13], [29, 46, 8], [43, 42, 35], [134, 132, 118], [92, 90, 82], [128, 125, 111], [39, 44, 26], [65, 72, 43], [43, 48, 29], nil, nil, nil, nil, nil],
24
- [nil, nil, nil, nil, [17, 19, 12], [63, 70, 42], [31, 35, 21], [5, 5, 4], [39, 39, 33], [70, 68, 58], [53, 53, 45], [103, 102, 85], [58, 56, 48], [77, 76, 65], [113, 109, 94], [171, 167, 151], [163, 160, 143], [108, 105, 91], [72, 70, 61], [64, 62, 54], [108, 105, 91], [59, 58, 50], [84, 82, 71], [39, 39, 33], [3, 3, 2], [39, 44, 26], [58, 64, 39], nil, nil, nil, nil, nil],
25
- [nil, nil, nil, [2, 3, 2], [43, 47, 28], [68, 75, 45], [15, 17, 11], [0, 0, 0], [0, 0, 0], [8, 8, 7], [12, 12, 11], [59, 58, 50], [92, 89, 75], [27, 26, 21], [100, 96, 82], [171, 167, 151], [163, 160, 143], [70, 68, 58], [44, 43, 36], [114, 111, 97], [58, 56, 48], [15, 14, 13], [5, 5, 4], [0, 0, 0], [0, 0, 0], [23, 25, 15], [69, 77, 46], [30, 33, 20], nil, nil, nil, nil],
26
- [nil, nil, nil, [37, 41, 26], [63, 70, 42], [27, 30, 19], [1, 1, 1], [0, 0, 0], [8, 8, 7], [32, 33, 30], [20, 20, 18], [6, 6, 5], [64, 62, 54], [31, 31, 25], [100, 96, 82], [171, 167, 151], [163, 160, 143], [70, 68, 58], [51, 49, 43], [70, 68, 58], [6, 6, 5], [23, 23, 21], [35, 35, 33], [6, 6, 5], [0, 0, 0], [1, 1, 1], [33, 37, 22], [58, 63, 37], [32, 35, 21], nil, nil, nil],
27
- [nil, nil, nil, [52, 58, 35], [80, 88, 53], [52, 58, 34], [10, 10, 8], [5, 5, 4], [32, 33, 30], [23, 23, 21], [0, 0, 0], [23, 23, 21], [59, 58, 50], [70, 68, 58], [72, 70, 61], [166, 162, 147], [153, 150, 134], [67, 65, 58], [76, 74, 65], [62, 61, 57], [19, 19, 17], [0, 0, 0], [31, 31, 25], [32, 33, 30], [3, 3, 2], [15, 17, 11], [55, 61, 36], [80, 88, 53], [39, 42, 25], nil, nil, nil],
28
- [nil, nil, nil, [23, 24, 16], [54, 60, 36], [83, 92, 55], [27, 30, 19], [44, 44, 40], [19, 19, 17], [1, 1, 1], [1, 1, 1], [49, 48, 45], [44, 44, 40], [103, 102, 85], [44, 44, 40], [158, 155, 140], [144, 140, 126], [64, 62, 54], [92, 89, 75], [49, 48, 45], [40, 40, 36], [0, 0, 0], [3, 3, 2], [19, 19, 17], [49, 48, 45], [36, 40, 24], [83, 92, 55], [44, 48, 30], [23, 25, 16], nil, nil, nil],
29
- [nil, nil, nil, [50, 55, 34], [49, 54, 33], [62, 68, 41], [67, 74, 44], [30, 33, 20], [12, 12, 11], [30, 30, 28], [20, 20, 18], [30, 30, 28], [12, 12, 11], [92, 89, 75], [51, 49, 43], [160, 156, 142], [144, 140, 126], [64, 62, 54], [77, 76, 65], [12, 12, 11], [35, 35, 33], [17, 17, 16], [35, 35, 33], [10, 10, 8], [34, 35, 26], [68, 75, 45], [58, 63, 37], [51, 56, 34], [43, 48, 29], nil, nil, nil],
30
- [nil, nil, [30, 33, 21], [68, 75, 45], [70, 78, 46], [39, 44, 26], [59, 65, 40], [67, 74, 44], [21, 23, 14], [8, 8, 7], [19, 19, 17], [32, 33, 30], [10, 10, 8], [15, 14, 13], [92, 89, 75], [168, 164, 149], [160, 157, 139], [70, 68, 58], [12, 12, 11], [10, 10, 8], [35, 35, 33], [17, 17, 16], [10, 10, 8], [21, 23, 14], [72, 80, 48], [55, 61, 36], [47, 52, 31], [72, 80, 48], [66, 72, 43], [25, 26, 18], nil, nil],
31
- [nil, nil, [22, 24, 14], [27, 30, 18], [55, 61, 36], [84, 93, 55], [44, 48, 30], [54, 60, 36], [74, 81, 48], [25, 28, 16], [1, 1, 1], [49, 48, 45], [32, 33, 30], [1, 1, 1], [92, 89, 75], [123, 119, 105], [118, 115, 104], [67, 63, 54], [1, 1, 1], [44, 44, 40], [40, 40, 36], [1, 1, 1], [30, 33, 20], [76, 85, 51], [52, 58, 34], [51, 56, 34], [81, 90, 54], [47, 52, 31], [26, 28, 17], nil, nil, nil],
32
- [nil, [30, 33, 21], [66, 72, 43], [80, 88, 53], [80, 88, 53], [45, 50, 29], [33, 37, 22], [51, 56, 34], [42, 47, 27], [68, 75, 45], [38, 42, 25], [19, 19, 17], [35, 35, 33], [12, 12, 11], [37, 35, 30], [70, 68, 58], [67, 63, 54], [27, 26, 21], [17, 17, 16], [35, 35, 33], [17, 17, 16], [39, 44, 26], [70, 78, 46], [42, 47, 27], [54, 60, 36], [31, 35, 21], [31, 35, 21], [63, 70, 42], [78, 86, 52], [65, 71, 42], nil, nil],
33
- [nil, [14, 15, 9], [36, 39, 23], [63, 70, 42], [81, 90, 54], [62, 68, 41], [51, 56, 34], [39, 44, 26], [14, 15, 9], [49, 54, 33], [69, 77, 46], [25, 28, 16], [20, 20, 18], [8, 8, 7], [6, 6, 5], [43, 42, 35], [37, 35, 30], [3, 3, 2], [12, 12, 11], [19, 19, 17], [30, 33, 20], [70, 78, 46], [44, 48, 30], [14, 15, 9], [44, 48, 30], [49, 54, 33], [52, 58, 34], [72, 80, 48], [54, 60, 36], [36, 39, 23], [12, 14, 11], nil],
34
- [nil, [5, 5, 5], nil, [53, 58, 35], [84, 93, 55], [84, 93, 55], [81, 90, 54], [69, 77, 46], [55, 61, 36], [15, 17, 11], [39, 44, 26], [68, 75, 45], [59, 65, 40], [18, 19, 12], [10, 10, 8], [62, 61, 57], [56, 55, 53], [6, 6, 5], [23, 25, 15], [63, 70, 42], [69, 77, 46], [36, 40, 24], [15, 17, 11], [58, 63, 37], [67, 74, 44], [80, 88, 53], [84, 93, 55], [83, 92, 55], [39, 43, 26], nil, [2, 2, 1], nil],
35
- [nil, nil, nil, [25, 29, 18], [45, 50, 29], [27, 30, 19], [22, 25, 15], [75, 83, 50], [84, 93, 55], [54, 60, 36], [15, 17, 11], [18, 19, 12], [42, 47, 27], [74, 81, 48], [12, 12, 11], [23, 23, 21], [19, 19, 17], [30, 33, 20], [74, 81, 48], [38, 42, 25], [15, 17, 11], [15, 17, 11], [62, 68, 41], [83, 92, 55], [21, 23, 14], [33, 37, 22], [49, 54, 33], [21, 23, 14], nil, nil, nil, [4, 4, 4]],
36
- [nil, nil, nil, [64, 71, 42], [43, 47, 28], [7, 7, 4], nil, [51, 56, 35], [83, 92, 55], [70, 78, 46], [47, 52, 31], [38, 42, 25], [31, 35, 21], [54, 60, 36], [17, 17, 16], [51, 51, 48], [46, 46, 43], [27, 30, 19], [52, 58, 34], [30, 33, 20], [39, 44, 26], [49, 54, 33], [76, 85, 51], [85, 93, 56], [43, 48, 29], [14, 14, 10], [8, 8, 7], [49, 54, 33], [62, 67, 41], nil, nil, nil],
37
- [nil, nil, nil, [34, 38, 24], [37, 39, 24], nil, nil, [34, 37, 22], [75, 83, 50], [35, 39, 24], [1, 1, 1], [43, 48, 29], [66, 72, 43], [20, 21, 12], [3, 1, 1], [2, 2, 2], [2, 2, 2], [3, 3, 3], [27, 30, 18], [66, 72, 43], [35, 38, 23], [2, 1, 1], [42, 46, 28], [76, 85, 51], [5, 5, 4], nil, [2, 3, 2], [36, 40, 24], [22, 25, 15], nil, nil, nil],
38
- [nil, nil, nil, nil, nil, nil, [14, 15, 10], [67, 74, 44], [29, 32, 19], nil, nil, nil, [25, 29, 18], nil, nil, nil, nil, nil, nil, [23, 25, 16], nil, nil, nil, [29, 32, 19], [52, 58, 35], nil, nil, nil, nil, nil, nil, nil],
39
- [nil, nil, nil, nil, nil, nil, nil, [22, 24, 15], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, [23, 26, 16], nil, nil, nil, nil, nil, nil, nil],
40
- [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],
41
- ]
42
-
43
- PIXELS.each do |row|
44
- row.each { |color| color.freeze if color }
45
- row.freeze
46
- end
47
- PIXELS.freeze
48
- end
49
- end
50
- end
@@ -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