kward 0.82.0 → 0.83.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 +28 -0
- data/Gemfile.lock +2 -2
- data/doc/agent-tools.md +1 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +18 -3
- data/doc/editor.md +20 -10
- data/doc/files.md +4 -3
- data/doc/permissions.md +1 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +1 -1
- data/doc/shell.md +40 -16
- data/doc/tabs.md +3 -0
- data/doc/usage.md +3 -2
- data/lib/kward/agent.rb +1 -0
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/interactive_turn.rb +101 -19
- data/lib/kward/cli/runtime_helpers.rb +156 -14
- data/lib/kward/cli/slash_commands.rb +12 -1
- data/lib/kward/cli/tabs.rb +12 -5
- data/lib/kward/cli.rb +10 -0
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +17 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/ekwsh.rb +70 -10
- data/lib/kward/model/client.rb +1 -17
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/persistent_shell_session.rb +750 -0
- data/lib/kward/project_files.rb +18 -5
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/editor/auto_indent.rb +3 -0
- data/lib/kward/prompt_interface/editor/controller.rb +69 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +10 -4
- data/lib/kward/prompt_interface/editor/renderer.rb +1 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -0
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +21 -7
- data/lib/kward/prompt_interface/key_handler.rb +8 -0
- data/lib/kward/prompt_interface/project_browser.rb +30 -5
- data/lib/kward/prompt_interface/runtime_state.rb +5 -1
- data/lib/kward/prompt_interface.rb +76 -0
- data/lib/kward/prompts/commands.rb +1 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/shell_prompt.rb +50 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +1 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +63 -5
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/version.rb +1 -1
- data/templates/default/layout/html/layout.erb +1 -1
- metadata +9 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Builds the model-facing request for an in-memory editor prompt.
|
|
4
|
+
module EditorPrompt
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def system_message
|
|
8
|
+
{
|
|
9
|
+
role: "system",
|
|
10
|
+
content: <<~PROMPT.strip
|
|
11
|
+
You are Kward's in-memory editor assistant.
|
|
12
|
+
|
|
13
|
+
Act only on the user's explicit request about the active editor buffer. Do not volunteer changes, explanations, or unrelated work. For an actionable edit request, call replace_editor_buffer with the complete replacement buffer. For a request that is only a question or needs clarification, answer briefly without changing the buffer. Never write files to disk and never claim that the buffer was saved.
|
|
14
|
+
PROMPT
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def input(instruction, context)
|
|
19
|
+
document = context[:display_path].to_s
|
|
20
|
+
language = context[:language].to_s
|
|
21
|
+
language = "text" if language.empty?
|
|
22
|
+
content = context[:content].to_s
|
|
23
|
+
|
|
24
|
+
<<~PROMPT
|
|
25
|
+
You are operating inside an interactive editor.
|
|
26
|
+
|
|
27
|
+
The user requested:
|
|
28
|
+
#{instruction}
|
|
29
|
+
|
|
30
|
+
Active document: #{document.empty? ? "(untitled)" : document}
|
|
31
|
+
Language: #{language}
|
|
32
|
+
Cursor: line #{context.dig(:cursor, 0).to_i + 1}, column #{context.dig(:cursor, 1).to_i + 1}
|
|
33
|
+
Selected text: #{context[:selection].to_s.empty? ? "(none)" : "present; full-buffer replacement is required"}
|
|
34
|
+
|
|
35
|
+
Current buffer:
|
|
36
|
+
<editor_buffer>
|
|
37
|
+
#{content}
|
|
38
|
+
</editor_buffer>
|
|
39
|
+
|
|
40
|
+
Satisfy the user's request by calling replace_editor_buffer with the complete desired buffer contents.
|
|
41
|
+
The operation changes only the in-memory editor buffer. Do not write files to disk.
|
|
42
|
+
If the request is ambiguous, explain the ambiguity instead of changing the buffer.
|
|
43
|
+
PROMPT
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Transactional draft used while an agent works on an active editor buffer.
|
|
4
|
+
#
|
|
5
|
+
# The live terminal editor remains owned by PromptInterface. Agent tool calls
|
|
6
|
+
# update this draft and the caller commits it only after a successful turn.
|
|
7
|
+
class EditorPromptSession
|
|
8
|
+
attr_reader :context, :content
|
|
9
|
+
|
|
10
|
+
def initialize(context)
|
|
11
|
+
@context = context.dup
|
|
12
|
+
@initial_content = @context.fetch(:content).to_s.dup
|
|
13
|
+
@content = @initial_content.dup
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def replace(content)
|
|
17
|
+
@content = content.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def changed?
|
|
21
|
+
@content != @initial_content
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initial_content
|
|
25
|
+
@initial_content.dup
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/kward/ekwsh.rb
CHANGED
|
@@ -18,17 +18,16 @@ module Kward
|
|
|
18
18
|
DEFAULT_TIMEOUT_SECONDS = 300
|
|
19
19
|
DEFAULT_MAX_OUTPUT_BYTES = 1_048_576
|
|
20
20
|
DEFAULT_HISTORY_LIMIT = 1_000
|
|
21
|
+
CONTEXT_OUTPUT_BYTES = 32_000
|
|
21
22
|
|
|
22
|
-
attr_reader :cwd
|
|
23
|
+
attr_reader :cwd, :last_command, :timeout_seconds
|
|
23
24
|
|
|
24
25
|
def command_shell
|
|
25
26
|
@shell
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def child_env(interactive: false)
|
|
29
|
-
|
|
30
|
-
env.delete("GIT_PAGER") if interactive && @defaulted_git_pager
|
|
31
|
-
env
|
|
30
|
+
@env.dup
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def initialize(cwd: Dir.pwd, env: ENV.to_h, shell: DEFAULT_SHELL, configured_env: {}, aliases: {}, timeout_seconds: DEFAULT_TIMEOUT_SECONDS, max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES)
|
|
@@ -43,6 +42,8 @@ module Kward
|
|
|
43
42
|
@shell = shell.to_s.empty? ? DEFAULT_SHELL : shell.to_s
|
|
44
43
|
@timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : DEFAULT_TIMEOUT_SECONDS
|
|
45
44
|
@max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : DEFAULT_MAX_OUTPUT_BYTES
|
|
45
|
+
@last_command = nil
|
|
46
|
+
@last_result = nil
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def prompt_label
|
|
@@ -53,7 +54,45 @@ module Kward
|
|
|
53
54
|
command = input.to_s.strip
|
|
54
55
|
return Result.new(output: "", exit_status: 0) if command.empty?
|
|
55
56
|
|
|
56
|
-
run_expanded_command(command, cancellation: cancellation, &block)
|
|
57
|
+
result = run_expanded_command(command, cancellation: cancellation, &block)
|
|
58
|
+
remember_result(command, result)
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Runs a command for the shell assistant without handing it the terminal.
|
|
63
|
+
# Built-ins still execute in this Ekwsh instance so their state persists.
|
|
64
|
+
def run_for_agent(input, timeout_seconds: nil, cancellation: nil)
|
|
65
|
+
command = input.to_s.strip
|
|
66
|
+
return run(command, cancellation: cancellation) if command.empty?
|
|
67
|
+
|
|
68
|
+
result = run(command, cancellation: cancellation)
|
|
69
|
+
return result unless result.interactive_command
|
|
70
|
+
|
|
71
|
+
result = execute(
|
|
72
|
+
result.interactive_command,
|
|
73
|
+
display_command: command,
|
|
74
|
+
timeout_seconds: timeout_seconds,
|
|
75
|
+
cancellation: cancellation
|
|
76
|
+
)
|
|
77
|
+
remember_result(command, result)
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Records the output of an interactive command after terminal handoff.
|
|
82
|
+
def record_interactive_result(output:, exit_status:)
|
|
83
|
+
return unless @last_command
|
|
84
|
+
|
|
85
|
+
@last_result = Result.new(output: ANSI.sanitize_transcript(output.to_s), exit_status: exit_status)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns bounded shell state for an explicit shell-agent prompt.
|
|
89
|
+
def context_snapshot(max_output_bytes: CONTEXT_OUTPUT_BYTES)
|
|
90
|
+
{
|
|
91
|
+
cwd: @cwd,
|
|
92
|
+
last_command: @last_command,
|
|
93
|
+
exit_status: @last_result&.exit_status,
|
|
94
|
+
last_output: context_output(@last_result&.output, max_output_bytes)
|
|
95
|
+
}
|
|
57
96
|
end
|
|
58
97
|
|
|
59
98
|
def complete(input, cursor)
|
|
@@ -112,8 +151,6 @@ module Kward
|
|
|
112
151
|
def configure_color_environment
|
|
113
152
|
@env["CLICOLOR"] ||= "1"
|
|
114
153
|
@env["COLORTERM"] ||= "truecolor"
|
|
115
|
-
@defaulted_git_pager = !@env.key?("GIT_PAGER")
|
|
116
|
-
@env["GIT_PAGER"] ||= "cat"
|
|
117
154
|
@env["TERM"] = "xterm-256color" if @env["TERM"].to_s.empty? || @env["TERM"] == "dumb"
|
|
118
155
|
end
|
|
119
156
|
|
|
@@ -522,12 +559,13 @@ module Kward
|
|
|
522
559
|
key.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
523
560
|
end
|
|
524
561
|
|
|
525
|
-
def execute(command, display_command: command, cancellation: nil)
|
|
562
|
+
def execute(command, display_command: command, timeout_seconds: nil, cancellation: nil)
|
|
526
563
|
output = command_echo(display_command)
|
|
527
564
|
streamed = block_given?
|
|
528
565
|
yield output.dup if streamed
|
|
566
|
+
timeout = effective_timeout(timeout_seconds)
|
|
529
567
|
result = external_command_runner.new(
|
|
530
|
-
timeout_seconds:
|
|
568
|
+
timeout_seconds: timeout,
|
|
531
569
|
max_output_bytes: @max_output_bytes,
|
|
532
570
|
terminate_on_output_limit: true
|
|
533
571
|
).run(@shell, "-c", command, env: @env, cwd: @cwd, cancellation: cancellation) do |_stream, chunk|
|
|
@@ -537,7 +575,7 @@ module Kward
|
|
|
537
575
|
end
|
|
538
576
|
append_output_newline(output) { |text| yield text if streamed }
|
|
539
577
|
exit_status = result.timed_out || result.truncated ? 1 : (result.exit_status || 1)
|
|
540
|
-
append_streamed(output, "ekwsh: command timed out after #{
|
|
578
|
+
append_streamed(output, "ekwsh: command timed out after #{timeout} seconds\n", streamed) { |text| yield text } if result.timed_out
|
|
541
579
|
append_streamed(output, "ekwsh: output exceeded #{@max_output_bytes} bytes; command terminated\n", streamed) { |text| yield text } if result.truncated
|
|
542
580
|
append_streamed(output, "Exit status: #{exit_status}\n", streamed) { |text| yield text } unless exit_status.zero?
|
|
543
581
|
Result.new(output: output, exit_status: exit_status, streamed: streamed)
|
|
@@ -549,6 +587,28 @@ module Kward
|
|
|
549
587
|
Result.new(output: "#{command_echo(display_command)}ekwsh: #{e.message}\n", exit_status: 127)
|
|
550
588
|
end
|
|
551
589
|
|
|
590
|
+
def effective_timeout(timeout_seconds)
|
|
591
|
+
timeout = timeout_seconds.to_i
|
|
592
|
+
timeout.positive? ? timeout : @timeout_seconds
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def remember_result(command, result)
|
|
596
|
+
@last_command = command.to_s
|
|
597
|
+
@last_result = result
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def context_output(output, max_output_bytes)
|
|
601
|
+
text = ANSI.sanitize_transcript(output.to_s)
|
|
602
|
+
limit = max_output_bytes.to_i
|
|
603
|
+
return text if limit <= 0 || text.bytesize <= limit
|
|
604
|
+
|
|
605
|
+
prefix = "[Earlier output omitted; showing the end of the command output.]\n"
|
|
606
|
+
tail_limit = [limit - prefix.bytesize, 0].max
|
|
607
|
+
tail = text.byteslice(-tail_limit, tail_limit).to_s if tail_limit.positive?
|
|
608
|
+
value = "#{prefix}#{tail}"
|
|
609
|
+
ANSI.normalize_transcript_encoding(value.byteslice(-limit, limit).to_s)
|
|
610
|
+
end
|
|
611
|
+
|
|
552
612
|
def external_command_runner
|
|
553
613
|
defined?(LocalPtyCommandRunner) ? LocalPtyCommandRunner : LocalCommandRunner
|
|
554
614
|
end
|
data/lib/kward/model/client.rb
CHANGED
|
@@ -906,7 +906,7 @@ module Kward
|
|
|
906
906
|
request["Content-Type"] = "application/json"
|
|
907
907
|
request["Accept"] = "text/event-stream"
|
|
908
908
|
request.body = request_body || JSON.dump(codex_payload(messages, tools, max_tokens: max_tokens))
|
|
909
|
-
|
|
909
|
+
request["originator"] = "kward"
|
|
910
910
|
|
|
911
911
|
message = nil
|
|
912
912
|
Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: stream_idle_timeout_seconds) do |http|
|
|
@@ -931,22 +931,6 @@ module Kward
|
|
|
931
931
|
raise e
|
|
932
932
|
end
|
|
933
933
|
|
|
934
|
-
def apply_codex_identity(request, luna:)
|
|
935
|
-
request["originator"] = "kward"
|
|
936
|
-
return unless luna
|
|
937
|
-
|
|
938
|
-
# TODO: Remove this Luna-specific Responses Lite workaround when Codex accepts Kward's own client identity.
|
|
939
|
-
request["originator"] = "codex_cli_rs"
|
|
940
|
-
request["User-Agent"] = "codex_cli_rs/0.144.1"
|
|
941
|
-
request["x-openai-internal-codex-responses-lite"] = "true"
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
def luna_request?(request_body)
|
|
945
|
-
JSON.parse(request_body)["model"] == "gpt-5.6-luna"
|
|
946
|
-
rescue JSON::ParserError
|
|
947
|
-
false
|
|
948
|
-
end
|
|
949
|
-
|
|
950
934
|
def parse_codex_sse(body, on_reasoning_delta: nil, on_reasoning_boundary: nil, on_assistant_delta: nil)
|
|
951
935
|
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)
|
|
952
936
|
end
|
|
@@ -12,6 +12,7 @@ module Kward
|
|
|
12
12
|
OPENAI_MODEL_CHOICES = %w[gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
|
|
13
13
|
ANTHROPIC_MODEL_CHOICES = %w[
|
|
14
14
|
claude-fable-5
|
|
15
|
+
claude-opus-5
|
|
15
16
|
claude-opus-4-8
|
|
16
17
|
claude-sonnet-5
|
|
17
18
|
claude-sonnet-4-6
|
|
@@ -113,7 +114,7 @@ module Kward
|
|
|
113
114
|
[/\Agpt-3\.5-turbo/, 16_385]
|
|
114
115
|
].freeze
|
|
115
116
|
ANTHROPIC_CONTEXT_WINDOWS = [
|
|
116
|
-
[/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)/, 1_000_000],
|
|
117
|
+
[/\Aclaude-(?:fable|mythos|opus|sonnet)-5(?:\z|-)/, 1_000_000],
|
|
117
118
|
[/\Aclaude-opus-4-(?:6|7|8)(?:\z|-)/, 1_000_000],
|
|
118
119
|
[/\Aclaude-sonnet-4-6(?:\z|-)/, 1_000_000],
|
|
119
120
|
[/\Aclaude-(?:haiku|opus|sonnet)-4-5(?:\z|-)/, 200_000],
|
|
@@ -454,7 +455,7 @@ module Kward
|
|
|
454
455
|
|
|
455
456
|
def anthropic_reasoning_effort_choices(id)
|
|
456
457
|
text = normalize_anthropic_model(id)
|
|
457
|
-
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos|sonnet)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
458
|
+
return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos|opus|sonnet)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/)
|
|
458
459
|
return ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:opus-4-6|sonnet-4-6)(?:\z|-)/)
|
|
459
460
|
return ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-opus-4-5(?:\z|-)/)
|
|
460
461
|
|
data/lib/kward/model/payloads.rb
CHANGED
|
@@ -189,8 +189,6 @@ module Kward
|
|
|
189
189
|
unless reasoning == false
|
|
190
190
|
effort = reasoning || reasoning_effort(provider)
|
|
191
191
|
payload[:reasoning] = { effort: effort, summary: "auto" }
|
|
192
|
-
# TODO: Remove this Luna-specific Responses Lite workaround when Codex accepts Kward's own client identity.
|
|
193
|
-
payload[:reasoning][:context] = "all_turns" if provider == "Codex" && parts[:model] == "gpt-5.6-luna"
|
|
194
192
|
end
|
|
195
193
|
payload
|
|
196
194
|
end
|