kward 0.76.0 → 0.78.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -1
  3. data/CHANGELOG.md +62 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +13 -4
  6. data/doc/agent-tools.md +0 -3
  7. data/doc/api.md +4 -0
  8. data/doc/code-search.md +9 -6
  9. data/doc/composer.md +219 -0
  10. data/doc/configuration.md +114 -27
  11. data/doc/editor.md +18 -2
  12. data/doc/files.md +1 -1
  13. data/doc/getting-started.md +4 -0
  14. data/doc/git.md +2 -2
  15. data/doc/lifecycle-hooks.md +2 -14
  16. data/doc/pan.md +167 -0
  17. data/doc/permissions.md +179 -0
  18. data/doc/plugins.md +58 -0
  19. data/doc/rpc.md +61 -22
  20. data/doc/security.md +184 -0
  21. data/doc/tabs.md +4 -3
  22. data/doc/usage.md +15 -32
  23. data/doc/web-search.md +4 -2
  24. data/lib/kward/ansi.rb +2 -2
  25. data/lib/kward/auth/anthropic_oauth.rb +2 -2
  26. data/lib/kward/auth/github_oauth.rb +3 -3
  27. data/lib/kward/auth/oauth_helpers.rb +4 -2
  28. data/lib/kward/auth/openai_oauth.rb +2 -1
  29. data/lib/kward/cancellation.rb +2 -2
  30. data/lib/kward/cli/commands.rb +1 -3
  31. data/lib/kward/cli/interactive_turn.rb +0 -17
  32. data/lib/kward/cli/plugins.rb +24 -7
  33. data/lib/kward/cli/prompt_interface.rb +1 -0
  34. data/lib/kward/cli/rendering.rb +7 -1
  35. data/lib/kward/cli/runtime_helpers.rb +11 -35
  36. data/lib/kward/cli/sessions.rb +14 -5
  37. data/lib/kward/cli/settings.rb +22 -0
  38. data/lib/kward/cli/slash_commands.rb +1 -561
  39. data/lib/kward/cli/tabs.rb +154 -36
  40. data/lib/kward/cli.rb +30 -26
  41. data/lib/kward/compactor.rb +3 -3
  42. data/lib/kward/config_files.rb +28 -0
  43. data/lib/kward/ekwsh.rb +0 -4
  44. data/lib/kward/hooks/catalog.rb +0 -5
  45. data/lib/kward/hooks/http_handler.rb +2 -1
  46. data/lib/kward/http.rb +18 -0
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +33 -11
  49. data/lib/kward/model/payloads.rb +6 -1
  50. data/lib/kward/openrouter_model_cache.rb +2 -1
  51. data/lib/kward/pan/index.html.erb +459 -64
  52. data/lib/kward/pan/kward_logo.png +0 -0
  53. data/lib/kward/pan/server.rb +248 -23
  54. data/lib/kward/permissions/policy.rb +171 -0
  55. data/lib/kward/plugin_registry.rb +71 -1
  56. data/lib/kward/prompt_interface/approval_prompt.rb +62 -0
  57. data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
  58. data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
  59. data/lib/kward/prompt_interface/layout.rb +0 -4
  60. data/lib/kward/prompt_interface/project_browser.rb +40 -1
  61. data/lib/kward/prompt_interface/question_prompt.rb +12 -3
  62. data/lib/kward/prompt_interface/runtime_state.rb +4 -0
  63. data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
  64. data/lib/kward/prompt_interface/transcript_renderer.rb +0 -12
  65. data/lib/kward/prompt_interface.rb +20 -1
  66. data/lib/kward/prompts/commands.rb +1 -3
  67. data/lib/kward/prompts.rb +20 -0
  68. data/lib/kward/rpc/plugin_chat_manager.rb +299 -0
  69. data/lib/kward/rpc/server.rb +48 -41
  70. data/lib/kward/rpc/session_manager.rb +76 -25
  71. data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
  72. data/lib/kward/rpc/tool_metadata.rb +23 -0
  73. data/lib/kward/rpc/transcript_normalizer.rb +18 -5
  74. data/lib/kward/session_store.rb +55 -16
  75. data/lib/kward/skills/registry.rb +21 -1
  76. data/lib/kward/starter_pack_installer.rb +3 -1
  77. data/lib/kward/tab_driver.rb +87 -0
  78. data/lib/kward/tab_store.rb +74 -12
  79. data/lib/kward/tools/base.rb +14 -0
  80. data/lib/kward/tools/code_search.rb +8 -2
  81. data/lib/kward/tools/fetch_content.rb +4 -2
  82. data/lib/kward/tools/fetch_raw.rb +3 -1
  83. data/lib/kward/tools/registry.rb +45 -17
  84. data/lib/kward/tools/search/code.rb +47 -13
  85. data/lib/kward/tools/search/web.rb +61 -18
  86. data/lib/kward/tools/search/web_fetch.rb +206 -38
  87. data/lib/kward/tools/tool_call.rb +0 -4
  88. data/lib/kward/tools/web_search.rb +3 -1
  89. data/lib/kward/transcript_export.rb +1 -1
  90. data/lib/kward/update_check.rb +2 -1
  91. data/lib/kward/version.rb +1 -1
  92. data/templates/default/kward_navigation.rb +7 -2
  93. data/templates/default/layout/html/layout.erb +2 -2
  94. data/templates/default/layout/html/setup.rb +1 -0
  95. metadata +11 -12
  96. data/lib/kward/workers/git_guard.rb +0 -93
  97. data/lib/kward/workers/job.rb +0 -99
  98. data/lib/kward/workers/live_view.rb +0 -49
  99. data/lib/kward/workers/manager.rb +0 -328
  100. data/lib/kward/workers/queue_runner.rb +0 -166
  101. data/lib/kward/workers/queue_store.rb +0 -112
  102. data/lib/kward/workers/store.rb +0 -72
  103. data/lib/kward/workers/tool_policy.rb +0 -23
  104. data/lib/kward/workers/worker.rb +0 -82
  105. data/lib/kward/workers/write_lock.rb +0 -38
  106. data/lib/kward/workers.rb +0 -10
@@ -45,11 +45,6 @@ module Kward
45
45
  EventDefinition.new(name: "git_stage_after", failure_policy: "warn"),
46
46
  EventDefinition.new(name: "git_commit_before", failure_policy: "deny"),
47
47
  EventDefinition.new(name: "git_commit_after", failure_policy: "warn"),
48
- EventDefinition.new(name: "worker_job_create", failure_policy: "warn"),
49
- EventDefinition.new(name: "worker_job_start_before", failure_policy: "warn"),
50
- EventDefinition.new(name: "worker_job_start_after", failure_policy: "warn"),
51
- EventDefinition.new(name: "worker_job_ready_for_review", failure_policy: "warn"),
52
- EventDefinition.new(name: "worker_job_failed", failure_policy: "warn")
53
48
  ].each_with_object({}) { |definition, result| result[definition.name] = definition }.freeze
54
49
 
55
50
  module_function
@@ -3,6 +3,7 @@ require "net/http"
3
3
  require "uri"
4
4
  require_relative "catalog"
5
5
  require_relative "decision"
6
+ require_relative "../http"
6
7
 
7
8
  # Namespace for the Kward CLI agent runtime.
8
9
  module Kward
@@ -33,7 +34,7 @@ module Kward
33
34
  private
34
35
 
35
36
  def post(body)
36
- request = Net::HTTP::Post.new(@uri)
37
+ request = Http.apply_user_agent(Net::HTTP::Post.new(@uri))
37
38
  request["Content-Type"] = "application/json"
38
39
  request["Accept"] = "application/json"
39
40
  @headers.each { |key, value| request[key] = value }
data/lib/kward/http.rb ADDED
@@ -0,0 +1,18 @@
1
+ require_relative "version"
2
+
3
+ # Namespace for the Kward CLI agent runtime.
4
+ module Kward
5
+ # Shared HTTP request headers for Kward-owned network traffic.
6
+ module Http
7
+ module_function
8
+
9
+ def user_agent
10
+ "Kward/#{VERSION}"
11
+ end
12
+
13
+ def apply_user_agent(request)
14
+ request["User-Agent"] = user_agent
15
+ request
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  require "base64"
2
- require "cgi"
2
+ require "cgi/escape"
3
3
  require "shellwords"
4
4
  require "tmpdir"
5
5
  require "uri"
@@ -6,6 +6,7 @@ require_relative "../auth/github_oauth"
6
6
  require_relative "../auth/openai_oauth"
7
7
  require_relative "../cancellation"
8
8
  require_relative "../config_files"
9
+ require_relative "../http"
9
10
  require_relative "../openrouter_model_cache"
10
11
  require_relative "context_overflow"
11
12
  require_relative "copilot_models"
@@ -35,6 +36,7 @@ module Kward
35
36
  DEFAULT_OPENAI_MODEL = ModelInfo::DEFAULT_OPENAI_MODEL
36
37
  DEFAULT_REASONING_EFFORT = ModelInfo::DEFAULT_REASONING_EFFORT
37
38
  RETRY_DELAYS = [1, 2].freeze
39
+ DEFAULT_STREAM_IDLE_TIMEOUT_SECONDS = 120
38
40
  NON_RETRYABLE_PROVIDER_LIMIT_PATTERNS = [
39
41
  /GoUsageLimitError/i,
40
42
  /FreeUsageLimitError/i,
@@ -335,7 +337,7 @@ module Kward
335
337
  end
336
338
 
337
339
  def chat_anthropic_provider(url:, token:, request_body:, current_model:, on_reasoning_delta:, on_assistant_delta:, cancellation:)
338
- request = Net::HTTP::Post.new(url)
340
+ request = Http.apply_user_agent(Net::HTTP::Post.new(url))
339
341
  request["Authorization"] = "Bearer #{token}"
340
342
  request["Content-Type"] = "application/json"
341
343
  request["Accept"] = "text/event-stream"
@@ -343,7 +345,7 @@ module Kward
343
345
  request.body = request_body
344
346
 
345
347
  message = nil
346
- Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: nil) do |http|
348
+ Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
347
349
  cancellation&.on_cancel { close_http(http) }
348
350
  cancellation&.raise_if_cancelled!
349
351
  http.request(request) do |response|
@@ -361,7 +363,7 @@ module Kward
361
363
  end
362
364
 
363
365
  def chat_openrouter_provider(url:, token:, request_body:, current_model:, on_assistant_delta:, cancellation:)
364
- request = Net::HTTP::Post.new(url)
366
+ request = Http.apply_user_agent(Net::HTTP::Post.new(url))
365
367
  request["Authorization"] = "Bearer #{token}"
366
368
  request["Content-Type"] = "application/json"
367
369
  request.body = request_body
@@ -547,7 +549,7 @@ module Kward
547
549
  return [] if token.empty?
548
550
 
549
551
  url = URI("#{@github_oauth.base_url}/models")
550
- request = Net::HTTP::Get.new(url)
552
+ request = Http.apply_user_agent(Net::HTTP::Get.new(url))
551
553
  request["Authorization"] = "Bearer #{token}"
552
554
  request["Accept"] = "application/json"
553
555
  copilot_headers([]).each { |key, value| request[key] = value }
@@ -577,7 +579,7 @@ module Kward
577
579
 
578
580
  def copilot_responses_chat(token, request_body:, on_assistant_delta: nil, cancellation: nil)
579
581
  url = URI("#{@github_oauth.base_url}/responses")
580
- request = Net::HTTP::Post.new(url)
582
+ request = Http.apply_user_agent(Net::HTTP::Post.new(url))
581
583
  request["Authorization"] = "Bearer #{token}"
582
584
  request["Content-Type"] = "application/json"
583
585
  request["Accept"] = "text/event-stream"
@@ -585,7 +587,7 @@ module Kward
585
587
  request.body = request_body
586
588
 
587
589
  message = nil
588
- Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: nil) do |http|
590
+ Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
589
591
  cancellation&.on_cancel { close_http(http) }
590
592
  cancellation&.raise_if_cancelled!
591
593
  http.request(request) do |response|
@@ -603,7 +605,7 @@ module Kward
603
605
  end
604
606
 
605
607
  def copilot_chat(url, token, messages, tools, request_body: nil, on_assistant_delta: nil, cancellation: nil)
606
- request = Net::HTTP::Post.new(url)
608
+ request = Http.apply_user_agent(Net::HTTP::Post.new(url))
607
609
  request["Authorization"] = "Bearer #{token}"
608
610
  request["Content-Type"] = "application/json"
609
611
  request["Accept"] = "text/event-stream"
@@ -633,7 +635,6 @@ module Kward
633
635
  "anthropic-version" => "2023-06-01",
634
636
  "anthropic-beta" => "claude-code-20250219,oauth-2025-04-20",
635
637
  "anthropic-dangerous-direct-browser-access" => "true",
636
- "user-agent" => "claude-cli/2.1.75",
637
638
  "x-app" => "cli"
638
639
  }
639
640
  end
@@ -653,16 +654,16 @@ module Kward
653
654
  end
654
655
 
655
656
  def codex_chat(url, token, account_id, messages, tools, request_body: nil, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil, cancellation: nil, max_tokens: nil)
656
- request = Net::HTTP::Post.new(url)
657
+ request = Http.apply_user_agent(Net::HTTP::Post.new(url))
657
658
  request["Authorization"] = "Bearer #{token}"
658
659
  request["ChatGPT-Account-Id"] = account_id if account_id
659
660
  request["Content-Type"] = "application/json"
660
661
  request["Accept"] = "text/event-stream"
661
- request["originator"] = "codex_cli_rs"
662
662
  request.body = request_body || JSON.dump(codex_payload(messages, tools, max_tokens: max_tokens))
663
+ apply_codex_identity(request, luna: luna_request?(request.body))
663
664
 
664
665
  message = nil
665
- Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: nil) do |http|
666
+ Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
666
667
  cancellation&.on_cancel { close_http(http) }
667
668
  cancellation&.raise_if_cancelled!
668
669
  http.request(request) do |response|
@@ -684,6 +685,22 @@ module Kward
684
685
  raise e
685
686
  end
686
687
 
688
+ def apply_codex_identity(request, luna:)
689
+ request["originator"] = "kward"
690
+ return unless luna
691
+
692
+ # TODO: Remove this Luna-specific Responses Lite workaround when Codex accepts Kward's own client identity.
693
+ request["originator"] = "codex_cli_rs"
694
+ request["User-Agent"] = "codex_cli_rs/0.144.1"
695
+ request["x-openai-internal-codex-responses-lite"] = "true"
696
+ end
697
+
698
+ def luna_request?(request_body)
699
+ JSON.parse(request_body)["model"] == "gpt-5.6-luna"
700
+ rescue JSON::ParserError
701
+ false
702
+ end
703
+
687
704
  def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil)
688
705
  ModelStreamParser.parse_codex_sse(body, on_reasoning_delta: on_reasoning_delta, on_reasoning_boundary: on_reasoning_boundary, on_assistant_delta: on_assistant_delta, show_raw_reasoning: codex_show_raw_reasoning?, usage_normalizer: method(:normalized_usage), request_error_class: RequestError)
689
706
  end
@@ -842,6 +859,11 @@ module Kward
842
859
  @config["codex_show_raw_reasoning"] == true
843
860
  end
844
861
 
862
+ def stream_idle_timeout_seconds
863
+ value = @config["stream_idle_timeout_seconds"].to_i
864
+ value.positive? ? value : DEFAULT_STREAM_IDLE_TIMEOUT_SECONDS
865
+ end
866
+
845
867
  def load_config
846
868
  ConfigFiles.read_config(@config_path)
847
869
  end
@@ -126,7 +126,12 @@ module Kward
126
126
  store: false,
127
127
  include: []
128
128
  }
129
- payload[:reasoning] = { effort: reasoning || reasoning_effort("Codex"), summary: "auto" } unless reasoning == false
129
+ unless reasoning == false
130
+ effort = reasoning || reasoning_effort("Codex")
131
+ payload[:reasoning] = { effort: effort, summary: "auto" }
132
+ # TODO: Remove this Luna-specific Responses Lite workaround when Codex accepts Kward's own client identity.
133
+ payload[:reasoning][:context] = "all_turns" if parts[:model] == "gpt-5.6-luna"
134
+ end
130
135
  payload
131
136
  end
132
137
 
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "time"
5
5
  require "uri"
6
6
  require_relative "config_files"
7
+ require_relative "http"
7
8
  require_relative "private_file"
8
9
 
9
10
  # Namespace for the Kward CLI agent runtime.
@@ -71,7 +72,7 @@ module Kward
71
72
  private
72
73
 
73
74
  def refresh_request
74
- Net::HTTP::Get.new(MODELS_URL).tap do |request|
75
+ Http.apply_user_agent(Net::HTTP::Get.new(MODELS_URL)).tap do |request|
75
76
  request["Authorization"] = "Bearer #{@api_key}"
76
77
  request["Accept"] = "application/json"
77
78
  end