Skip to content
Kward Search API index

Class: Kward::Kwsh

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/shell/kwsh.rb

Overview

Kward-native embedded shell command runner.

Defined Under Namespace

Classes: Completion, Result

Constant Summary collapse

BUILTINS =
%w[alias capture cd pwd export source unset unalias clear exit logout pty].freeze
DEFAULT_SHELL =
"/bin/sh"
DEFAULT_TIMEOUT_SECONDS =
300
DEFAULT_MAX_OUTPUT_BYTES =
1_048_576
DEFAULT_HISTORY_LIMIT =
1_000
CONTEXT_OUTPUT_BYTES =
32_000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#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) ⇒ Kwsh

Returns a new instance of Kwsh.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/kward/shell/kwsh.rb', line 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)
  @cwd = File.expand_path(cwd.to_s.empty? ? Dir.pwd : cwd.to_s)
  @previous_cwd = nil
  @env = env.to_h.transform_keys(&:to_s).transform_values(&:to_s)
  @env.merge!(configured_env.to_h.transform_keys(&:to_s).transform_values(&:to_s))
  @env["PWD"] = @cwd
  configure_rbenv_environment
  configure_color_environment
  @aliases = aliases.to_h.transform_keys(&:to_s).transform_values(&:to_s)
  @shell = shell.to_s.empty? ? DEFAULT_SHELL : shell.to_s
  @timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : DEFAULT_TIMEOUT_SECONDS
  @max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : DEFAULT_MAX_OUTPUT_BYTES
  @last_command = nil
  @last_result = nil
end

Instance Attribute Details

#cwdObject (readonly)

Returns the value of attribute cwd.



23
24
25
# File 'lib/kward/shell/kwsh.rb', line 23

def cwd
  @cwd
end

#last_commandObject (readonly)

Returns the value of attribute last_command.



23
24
25
# File 'lib/kward/shell/kwsh.rb', line 23

def last_command
  @last_command
end

#timeout_secondsObject (readonly)

Returns the value of attribute timeout_seconds.



23
24
25
# File 'lib/kward/shell/kwsh.rb', line 23

def timeout_seconds
  @timeout_seconds
end

Class Method Details

.valid_alias_name?(name) ⇒ Boolean

Returns:

  • (Boolean)


431
432
433
# File 'lib/kward/shell/kwsh.rb', line 431

def self.valid_alias_name?(name)
  name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/) && !BUILTINS.include?(name.to_s)
end

Instance Method Details

#child_env(interactive: false) ⇒ Object



29
30
31
# File 'lib/kward/shell/kwsh.rb', line 29

def child_env(interactive: false)
  @env.dup
end

#command_shellObject



25
26
27
# File 'lib/kward/shell/kwsh.rb', line 25

def command_shell
  @shell
end

#complete(input, cursor) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/kward/shell/kwsh.rb', line 98

def complete(input, cursor)
  token = completion_token(input.to_s, cursor.to_i)
  return nil if token[:command] && token[:text].empty?

  completion_text = token[:path_text] || token[:text]
  candidates = if token[:command] && !path_like_token?(completion_text) && !token[:quote]
                 command_candidates(completion_text)
               else
                 path_candidates(completion_text, directories_only: cd_completion?(input, token), quote: token[:quote])
               end
  return nil if candidates.empty?

  replacement = completion_replacement(token[:text], candidates)
  Completion.new(range: token[:range], replacement: replacement, candidates: candidates)
end

#context_snapshot(max_output_bytes: CONTEXT_OUTPUT_BYTES) ⇒ Object

Returns bounded shell state for an explicit shell-agent prompt.



89
90
91
92
93
94
95
96
# File 'lib/kward/shell/kwsh.rb', line 89

def context_snapshot(max_output_bytes: CONTEXT_OUTPUT_BYTES)
  {
    cwd: @cwd,
    last_command: @last_command,
    exit_status: @last_result&.exit_status,
    last_output: context_output(@last_result&.output, max_output_bytes)
  }
end

#editor_command_result(command, display_command: command) ⇒ Object



126
127
128
129
# File 'lib/kward/shell/kwsh.rb', line 126

def editor_command_result(command, display_command: command)
  expanded_command = expand_alias(command, interactive: true)
  kward_command_result(expanded_command, display_command: display_command)
end

#expand_alias(command, interactive: false) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/kward/shell/kwsh.rb', line 114

def expand_alias(command, interactive: false)
  words = shell_words(command)
  return command if words.empty? || BUILTINS.include?(words.first)
  return command unless @aliases[words.first]

  rest = command.sub(/\A\s*#{Regexp.escape(words.first)}\b\s*/, "")
  expanded = [@aliases.fetch(words.first), rest].reject(&:empty?).join(" ")
  interactive ? expanded.sub(/\A\s*(?:capture|pty)(?:\s+|\z)/, "") : expanded
rescue ArgumentError
  command
end

#prompt_labelObject



49
50
51
# File 'lib/kward/shell/kwsh.rb', line 49

def prompt_label
  "Shell #{display_cwd} $"
end

#record_interactive_result(output:, exit_status:) ⇒ Object

Records the output of an interactive command after terminal handoff.



82
83
84
85
86
# File 'lib/kward/shell/kwsh.rb', line 82

def record_interactive_result(output:, exit_status:)
  return unless @last_command

  @last_result = Result.new(output: ANSI.sanitize_transcript(output.to_s), exit_status: exit_status)
end

#run(input, cancellation: nil, &block) ⇒ Object



53
54
55
56
57
58
59
60
# File 'lib/kward/shell/kwsh.rb', line 53

def run(input, cancellation: nil, &block)
  command = input.to_s.strip
  return Result.new(output: "", exit_status: 0) if command.empty?

  result = run_expanded_command(command, cancellation: cancellation, &block)
  remember_result(command, result)
  result
end

#run_for_agent(input, timeout_seconds: nil, cancellation: nil) ⇒ Object

Runs a command for the shell assistant without handing it the terminal. Built-ins still execute in this Kwsh instance so their state persists.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kward/shell/kwsh.rb', line 64

def run_for_agent(input, timeout_seconds: nil, cancellation: nil)
  command = input.to_s.strip
  return run(command, cancellation: cancellation) if command.empty?

  result = run(command, cancellation: cancellation)
  return result unless result.interactive_command

  result = execute(
    result.interactive_command,
    display_command: command,
    timeout_seconds: timeout_seconds,
    cancellation: cancellation
  )
  remember_result(command, result)
  result
end