kward 0.66.0 → 0.67.1
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 +50 -3
- data/Gemfile.lock +2 -2
- data/README.md +5 -1
- data/doc/configuration.md +43 -1
- data/doc/memory.md +31 -9
- data/doc/rpc.md +41 -21
- data/doc/troubleshooting.md +55 -0
- data/doc/usage.md +41 -6
- data/lib/kward/cli.rb +1155 -195
- data/lib/kward/cli_transcript_formatter.rb +124 -0
- data/lib/kward/compaction/file_operation_tracker.rb +46 -0
- data/lib/kward/compactor.rb +3 -68
- data/lib/kward/config_files.rb +45 -69
- data/lib/kward/memory/manager.rb +66 -7
- data/lib/kward/model/client.rb +2 -195
- data/lib/kward/model/model_info.rb +9 -10
- data/lib/kward/model/payloads.rb +203 -0
- data/lib/kward/prompt_interface/banner.rb +77 -0
- data/lib/kward/prompt_interface.rb +220 -191
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/rpc/runtime_payloads.rb +79 -0
- data/lib/kward/rpc/server.rb +33 -34
- data/lib/kward/rpc/session_manager.rb +518 -159
- data/lib/kward/rpc/tool_event_normalizer.rb +12 -9
- data/lib/kward/rpc/transcript_normalizer.rb +31 -53
- data/lib/kward/session_store.rb +269 -23
- data/lib/kward/session_trash.rb +96 -0
- data/lib/kward/session_tree_renderer.rb +264 -0
- data/lib/kward/tools/registry.rb +3 -1
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +10 -5
- metadata +9 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
|
|
4
|
+
module Kward
|
|
5
|
+
# Best-effort session file remover. Uses the OS trash/recycle bin when a
|
|
6
|
+
# supported platform tool is available, and falls back to permanent deletion.
|
|
7
|
+
class SessionTrash
|
|
8
|
+
def initialize(env: ENV, command_runner: nil, host_os: RbConfig::CONFIG["host_os"])
|
|
9
|
+
@env = env
|
|
10
|
+
@command_runner = command_runner || method(:run_command)
|
|
11
|
+
@host_os = host_os
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def delete(path)
|
|
15
|
+
return false unless File.exist?(path)
|
|
16
|
+
|
|
17
|
+
return true if move_to_trash(path)
|
|
18
|
+
|
|
19
|
+
File.delete(path) if File.exist?(path)
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def move_to_trash(path)
|
|
26
|
+
trash_commands.each do |name, command|
|
|
27
|
+
next unless executable?(name)
|
|
28
|
+
|
|
29
|
+
return true if @command_runner.call(command, path) && !File.exist?(path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trash_commands
|
|
36
|
+
if windows?
|
|
37
|
+
powershell_trash_commands
|
|
38
|
+
elsif macos?
|
|
39
|
+
[["osascript", ["osascript", "-e", "on run argv", "-e", "tell application \"Finder\" to delete POSIX file (item 1 of argv)", "-e", "end run"]]]
|
|
40
|
+
else
|
|
41
|
+
[
|
|
42
|
+
["gio", ["gio", "trash"]],
|
|
43
|
+
["trash-put", ["trash-put"]],
|
|
44
|
+
["kioclient5", ["kioclient5", "move", nil, "trash:/"]],
|
|
45
|
+
["kioclient", ["kioclient", "move", nil, "trash:/"]]
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def powershell_trash_commands
|
|
51
|
+
script = <<~POWERSHELL.strip
|
|
52
|
+
Add-Type -AssemblyName Microsoft.VisualBasic;
|
|
53
|
+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($args[0], 'OnlyErrorDialogs', 'SendToRecycleBin')
|
|
54
|
+
POWERSHELL
|
|
55
|
+
[
|
|
56
|
+
["pwsh", ["pwsh", "-NoProfile", "-Command", script]],
|
|
57
|
+
["powershell", ["powershell", "-NoProfile", "-Command", script]]
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def executable?(name)
|
|
62
|
+
paths = @env.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
63
|
+
extensions = windows? ? @env.fetch("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") : [""]
|
|
64
|
+
paths.any? do |path|
|
|
65
|
+
extensions.any? do |extension|
|
|
66
|
+
candidate = File.join(path, name + extension)
|
|
67
|
+
File.file?(candidate) && File.executable?(candidate)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def run_command(command, path)
|
|
73
|
+
command = if command.include?(nil)
|
|
74
|
+
command.map { |part| part || path }
|
|
75
|
+
else
|
|
76
|
+
command + [path]
|
|
77
|
+
end
|
|
78
|
+
_stdout, _stderr, status = Open3.capture3(*command)
|
|
79
|
+
status.success?
|
|
80
|
+
rescue StandardError
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def macos?
|
|
85
|
+
host_os.match?(/darwin/i)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def windows?
|
|
89
|
+
host_os.match?(/mswin|mingw|cygwin/i)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def host_os
|
|
93
|
+
@host_os
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "message_access"
|
|
3
|
+
require_relative "tools/tool_call"
|
|
4
|
+
|
|
5
|
+
module Kward
|
|
6
|
+
class SessionTreeRenderer
|
|
7
|
+
def initialize(roots:, current_leaf_id:)
|
|
8
|
+
@roots = roots
|
|
9
|
+
@current_leaf_id = current_leaf_id
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def items
|
|
13
|
+
active_path = session_tree_active_path(@roots, @current_leaf_id)
|
|
14
|
+
tool_calls_by_id = session_tree_tool_calls(@roots)
|
|
15
|
+
visible_roots = @roots.flat_map { |root| visible_session_tree_nodes(root) }
|
|
16
|
+
multiple_roots = visible_roots.length > 1
|
|
17
|
+
result = []
|
|
18
|
+
|
|
19
|
+
walk = lambda do |node, indent, just_branched, show_connector, is_last, gutters, virtual_root_child|
|
|
20
|
+
entry = node[:source]["entry"] || {}
|
|
21
|
+
display_indent = multiple_roots ? [indent - 1, 0].max : indent
|
|
22
|
+
prefix = session_tree_visual_prefix(display_indent, gutters, show_connector && !virtual_root_child, is_last, !node[:children].empty?)
|
|
23
|
+
result << {
|
|
24
|
+
entry: entry,
|
|
25
|
+
label: session_tree_label(entry, node[:source], prefix, active_path.include?(entry["id"].to_s), tool_calls_by_id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
children = node[:children].sort_by { |child| session_tree_contains_active_path?(child, active_path) ? 0 : 1 }
|
|
29
|
+
multiple_children = children.length > 1
|
|
30
|
+
child_indent = if multiple_children
|
|
31
|
+
indent + 1
|
|
32
|
+
elsif just_branched && indent.positive?
|
|
33
|
+
indent + 1
|
|
34
|
+
else
|
|
35
|
+
indent
|
|
36
|
+
end
|
|
37
|
+
connector_position = [display_indent - 1, 0].max
|
|
38
|
+
child_gutters = show_connector && !virtual_root_child ? gutters + [{ position: connector_position, show: !is_last }] : gutters
|
|
39
|
+
|
|
40
|
+
children.each_with_index do |child, index|
|
|
41
|
+
walk.call(child, child_indent, multiple_children, multiple_children, index == children.length - 1, child_gutters, false)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
visible_roots.sort_by { |root| session_tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index do |root, index|
|
|
46
|
+
walk.call(root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def visible_session_tree_nodes(node)
|
|
55
|
+
children = Array(node["children"]).flat_map { |child| visible_session_tree_nodes(child) }
|
|
56
|
+
return children if hidden_session_tree_entry?(node["entry"] || {})
|
|
57
|
+
|
|
58
|
+
[{ source: node, children: children }]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hidden_session_tree_entry?(entry)
|
|
62
|
+
return false if @current_leaf_id && entry["id"].to_s == @current_leaf_id.to_s
|
|
63
|
+
return false unless entry["type"] == "message"
|
|
64
|
+
|
|
65
|
+
message = entry["message"]
|
|
66
|
+
return false unless message.is_a?(Hash) && message_role(message) == "assistant"
|
|
67
|
+
|
|
68
|
+
content = message_content(message)
|
|
69
|
+
content_tool_calls = content.is_a?(Array) && content.any? { |part| session_tree_content_part_value(part, :type) == "toolCall" }
|
|
70
|
+
(content_tool_calls && !session_tree_text_content?(content)) || (!message_tool_calls(message).empty? && full_message_text(message).empty?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def session_tree_text_content?(content)
|
|
74
|
+
Array(content).any? { |part| session_tree_content_part_value(part, :type) == "text" && session_tree_content_part_value(part, :text).to_s.strip != "" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def session_tree_content_part_value(part, key)
|
|
78
|
+
MessageAccess.value(part, key)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def session_tree_label(entry, node, prefix, active_path, tool_calls_by_id)
|
|
82
|
+
label = node["label"] || entry["resolvedLabel"]
|
|
83
|
+
label = label.to_s.strip
|
|
84
|
+
label_text = label.empty? ? "" : "[#{label}] "
|
|
85
|
+
path_marker = active_path ? "• " : ""
|
|
86
|
+
"#{prefix}#{path_marker}#{label_text}#{session_tree_entry_display(entry, tool_calls_by_id)}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def session_tree_entry_display(entry, tool_calls_by_id = {})
|
|
90
|
+
case entry["type"]
|
|
91
|
+
when "message"
|
|
92
|
+
message = entry["message"] || {}
|
|
93
|
+
role = message_role(message).to_s
|
|
94
|
+
return session_tree_tool_display(message, tool_calls_by_id) if ["tool", "toolResult"].include?(role)
|
|
95
|
+
|
|
96
|
+
"#{role.empty? ? 'message' : role}: #{display_message_text(message)}"
|
|
97
|
+
when "compaction"
|
|
98
|
+
"compaction: #{display_message_text(entry["message"] || {})}"
|
|
99
|
+
when "branch_summary"
|
|
100
|
+
"[branch summary]: #{truncate_session_tree_text(entry["summary"])}"
|
|
101
|
+
else
|
|
102
|
+
entry["type"].to_s
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def session_tree_visual_prefix(display_indent, gutters, show_connector, is_last, foldable)
|
|
107
|
+
return "" if display_indent.to_i <= 0
|
|
108
|
+
|
|
109
|
+
connector_position = show_connector ? display_indent - 1 : -1
|
|
110
|
+
(0...(display_indent * 3)).map do |index|
|
|
111
|
+
level = index / 3
|
|
112
|
+
position = index % 3
|
|
113
|
+
gutter = gutters.find { |candidate| candidate[:position] == level }
|
|
114
|
+
|
|
115
|
+
if gutter
|
|
116
|
+
position.zero? && gutter[:show] ? "│" : " "
|
|
117
|
+
elsif show_connector && level == connector_position
|
|
118
|
+
if position.zero?
|
|
119
|
+
is_last ? "└" : "├"
|
|
120
|
+
elsif position == 1
|
|
121
|
+
foldable ? "⊟" : "─"
|
|
122
|
+
else
|
|
123
|
+
" "
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
" "
|
|
127
|
+
end
|
|
128
|
+
end.join
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def session_tree_contains_active_path?(node, active_path)
|
|
132
|
+
entry_id = (node[:source]["entry"] || {})["id"].to_s
|
|
133
|
+
active_path.include?(entry_id) || node[:children].any? { |child| session_tree_contains_active_path?(child, active_path) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def session_tree_active_path(roots, leaf_id)
|
|
137
|
+
by_id = session_tree_entries_by_id(roots)
|
|
138
|
+
ids = []
|
|
139
|
+
entry = by_id[leaf_id.to_s]
|
|
140
|
+
while entry
|
|
141
|
+
ids << entry["id"].to_s
|
|
142
|
+
entry = by_id[entry["parentId"].to_s]
|
|
143
|
+
end
|
|
144
|
+
ids
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def session_tree_entries_by_id(roots)
|
|
148
|
+
roots.each_with_object({}) do |root, map|
|
|
149
|
+
stack = [root]
|
|
150
|
+
until stack.empty?
|
|
151
|
+
node = stack.pop
|
|
152
|
+
entry = node["entry"] || {}
|
|
153
|
+
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
154
|
+
stack.concat(Array(node["children"]))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def session_tree_tool_calls(roots)
|
|
160
|
+
roots.each_with_object({}) do |root, tool_calls|
|
|
161
|
+
stack = [root]
|
|
162
|
+
until stack.empty?
|
|
163
|
+
node = stack.pop
|
|
164
|
+
entry = node["entry"] || {}
|
|
165
|
+
message = entry["message"]
|
|
166
|
+
if entry["type"] == "message" && message.is_a?(Hash) && message_role(message) == "assistant"
|
|
167
|
+
message_tool_calls(message).each { |tool_call| tool_calls[tool_call_id(tool_call).to_s] = tool_call }
|
|
168
|
+
end
|
|
169
|
+
stack.concat(Array(node["children"]))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def session_tree_tool_display(message, tool_calls_by_id)
|
|
175
|
+
tool_call = tool_calls_by_id[session_tree_message_tool_call_id(message).to_s]
|
|
176
|
+
return session_tree_format_tool_call(tool_call) if tool_call
|
|
177
|
+
|
|
178
|
+
name = session_tree_message_tool_name(message).to_s
|
|
179
|
+
"[#{name.empty? ? 'tool' : name}]"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def session_tree_message_tool_call_id(message)
|
|
183
|
+
message_tool_call_id(message) || MessageAccess.value(message, :toolCallId)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def session_tree_message_tool_name(message)
|
|
187
|
+
message_name(message) || MessageAccess.value(message, :toolName)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def session_tree_format_tool_call(tool_call)
|
|
191
|
+
name = ToolCall.display_name(tool_call)
|
|
192
|
+
args = tool_call_args(tool_call)
|
|
193
|
+
case name
|
|
194
|
+
when "read"
|
|
195
|
+
path = args["path"] || args[:path] || args["file_path"] || args[:file_path]
|
|
196
|
+
offset = args["offset"] || args[:offset]
|
|
197
|
+
limit = args["limit"] || args[:limit]
|
|
198
|
+
display = path.to_s
|
|
199
|
+
if offset || limit
|
|
200
|
+
start_line = offset || 1
|
|
201
|
+
end_line = limit ? start_line.to_i + limit.to_i - 1 : nil
|
|
202
|
+
display += ":#{start_line}#{end_line ? "-#{end_line}" : ""}"
|
|
203
|
+
end
|
|
204
|
+
"[read: #{display}]"
|
|
205
|
+
when "write", "edit"
|
|
206
|
+
path = args["path"] || args[:path] || args["file_path"] || args[:file_path]
|
|
207
|
+
"[#{name}: #{path}]"
|
|
208
|
+
when "bash"
|
|
209
|
+
command = (args["command"] || args[:command]).to_s.gsub(/[\n\t]/, " ").strip
|
|
210
|
+
"[bash: #{command.length > 50 ? "#{command.slice(0, 50)}..." : command}]"
|
|
211
|
+
else
|
|
212
|
+
serialized = JSON.dump(args)
|
|
213
|
+
"[#{name}: #{serialized.length > 40 ? "#{serialized.slice(0, 40)}..." : serialized}]"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def display_message_text(message)
|
|
218
|
+
truncate_session_tree_text(full_message_text(message))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def truncate_session_tree_text(text)
|
|
222
|
+
normalized = text.to_s.gsub(/\s+/, " ").strip
|
|
223
|
+
normalized.length > 120 ? "#{normalized.slice(0, 117)}..." : normalized
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def full_message_text(message)
|
|
227
|
+
content = message_content(message)
|
|
228
|
+
text = if content.is_a?(Array)
|
|
229
|
+
content.filter_map { |part| MessageAccess.value(part, :text) }.join("\n")
|
|
230
|
+
else
|
|
231
|
+
content.to_s
|
|
232
|
+
end
|
|
233
|
+
text.strip
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def message_role(message)
|
|
237
|
+
MessageAccess.role(message)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def message_content(message)
|
|
241
|
+
MessageAccess.content(message)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def message_name(message)
|
|
245
|
+
MessageAccess.name(message)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def message_tool_call_id(message)
|
|
249
|
+
MessageAccess.tool_call_id(message)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def message_tool_calls(message)
|
|
253
|
+
MessageAccess.tool_calls(message)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def tool_call_id(tool_call)
|
|
257
|
+
ToolCall.id(tool_call)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def tool_call_args(tool_call)
|
|
261
|
+
ToolCall.arguments(tool_call)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
data/lib/kward/tools/registry.rb
CHANGED
|
@@ -69,7 +69,9 @@ module Kward
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def build_schema_tools
|
|
72
|
-
tools =
|
|
72
|
+
tools = @tools.values_at(
|
|
73
|
+
"list_directory", "read_file", "write_file", "edit_file", "run_shell_command", "code_search"
|
|
74
|
+
)
|
|
73
75
|
tools << @tools["web_search"] if web_search_available?
|
|
74
76
|
tools << @tools["read_skill"] if skills_available?
|
|
75
77
|
tools << @tools["ask_user_question"] if ask_user_question_available?
|
data/lib/kward/version.rb
CHANGED
data/lib/kward/workspace.rb
CHANGED
|
@@ -12,8 +12,9 @@ module Kward
|
|
|
12
12
|
MAX_EDIT_DIFF_BYTES = 8 * 1024
|
|
13
13
|
DEFAULT_COMMAND_TIMEOUT_SECONDS = 30
|
|
14
14
|
|
|
15
|
-
def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES)
|
|
15
|
+
def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES, guardrails: true)
|
|
16
16
|
@root = Pathname.new(root).realpath
|
|
17
|
+
@guardrails = guardrails
|
|
17
18
|
@max_file_bytes = max_file_bytes
|
|
18
19
|
@max_read_output_bytes = max_read_output_bytes
|
|
19
20
|
@max_read_output_lines = max_read_output_lines
|
|
@@ -124,10 +125,10 @@ module Kward
|
|
|
124
125
|
target = @root.join(target) unless target.absolute?
|
|
125
126
|
|
|
126
127
|
expanded = target.expand_path
|
|
127
|
-
raise SecurityError, "path outside workspace: #{path}"
|
|
128
|
+
raise SecurityError, "path outside workspace: #{path}" if guardrails_enabled? && !inside_workspace?(expanded)
|
|
128
129
|
|
|
129
130
|
resolved = target.realpath
|
|
130
|
-
raise SecurityError, "path outside workspace: #{path}"
|
|
131
|
+
raise SecurityError, "path outside workspace: #{path}" if guardrails_enabled? && !inside_workspace?(resolved)
|
|
131
132
|
|
|
132
133
|
resolved.to_s
|
|
133
134
|
end
|
|
@@ -137,16 +138,20 @@ module Kward
|
|
|
137
138
|
target = @root.join(target) unless target.absolute?
|
|
138
139
|
|
|
139
140
|
expanded = target.expand_path
|
|
140
|
-
raise SecurityError, "path outside workspace: #{path}"
|
|
141
|
+
raise SecurityError, "path outside workspace: #{path}" if guardrails_enabled? && !inside_workspace?(expanded)
|
|
141
142
|
|
|
142
143
|
return workspace_path(path) if File.exist?(expanded) || File.symlink?(expanded)
|
|
143
144
|
|
|
144
145
|
parent = expanded.dirname.realpath
|
|
145
|
-
raise SecurityError, "path outside workspace: #{path}"
|
|
146
|
+
raise SecurityError, "path outside workspace: #{path}" if guardrails_enabled? && !inside_workspace?(parent)
|
|
146
147
|
|
|
147
148
|
expanded.to_s
|
|
148
149
|
end
|
|
149
150
|
|
|
151
|
+
def guardrails_enabled?
|
|
152
|
+
@guardrails != false
|
|
153
|
+
end
|
|
154
|
+
|
|
150
155
|
def inside_workspace?(path)
|
|
151
156
|
path.to_s == @root.to_s || path.to_s.start_with?("#{@root}/")
|
|
152
157
|
end
|
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.
|
|
4
|
+
version: 0.67.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kai Wood
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- doc/plugins.md
|
|
133
133
|
- doc/releasing.md
|
|
134
134
|
- doc/rpc.md
|
|
135
|
+
- doc/troubleshooting.md
|
|
135
136
|
- doc/usage.md
|
|
136
137
|
- doc/web-search.md
|
|
137
138
|
- exe/kward
|
|
@@ -145,7 +146,9 @@ files:
|
|
|
145
146
|
- lib/kward/auth/openrouter_api_key.rb
|
|
146
147
|
- lib/kward/cancellation.rb
|
|
147
148
|
- lib/kward/cli.rb
|
|
149
|
+
- lib/kward/cli_transcript_formatter.rb
|
|
148
150
|
- lib/kward/clipboard.rb
|
|
151
|
+
- lib/kward/compaction/file_operation_tracker.rb
|
|
149
152
|
- lib/kward/compactor.rb
|
|
150
153
|
- lib/kward/config_files.rb
|
|
151
154
|
- lib/kward/conversation.rb
|
|
@@ -160,6 +163,7 @@ files:
|
|
|
160
163
|
- lib/kward/model/context_overflow.rb
|
|
161
164
|
- lib/kward/model/context_usage.rb
|
|
162
165
|
- lib/kward/model/model_info.rb
|
|
166
|
+
- lib/kward/model/payloads.rb
|
|
163
167
|
- lib/kward/model/retry_message.rb
|
|
164
168
|
- lib/kward/model/stream_parser.rb
|
|
165
169
|
- lib/kward/pan/index.html.erb
|
|
@@ -167,6 +171,7 @@ files:
|
|
|
167
171
|
- lib/kward/plugin_registry.rb
|
|
168
172
|
- lib/kward/private_file.rb
|
|
169
173
|
- lib/kward/prompt_interface.rb
|
|
174
|
+
- lib/kward/prompt_interface/banner.rb
|
|
170
175
|
- lib/kward/prompts.rb
|
|
171
176
|
- lib/kward/prompts/commands.rb
|
|
172
177
|
- lib/kward/prompts/templates.rb
|
|
@@ -176,6 +181,7 @@ files:
|
|
|
176
181
|
- lib/kward/rpc/config_manager.rb
|
|
177
182
|
- lib/kward/rpc/prompt_bridge.rb
|
|
178
183
|
- lib/kward/rpc/redactor.rb
|
|
184
|
+
- lib/kward/rpc/runtime_payloads.rb
|
|
179
185
|
- lib/kward/rpc/server.rb
|
|
180
186
|
- lib/kward/rpc/session_manager.rb
|
|
181
187
|
- lib/kward/rpc/tool_event_normalizer.rb
|
|
@@ -184,6 +190,8 @@ files:
|
|
|
184
190
|
- lib/kward/rpc/transport.rb
|
|
185
191
|
- lib/kward/session_diff.rb
|
|
186
192
|
- lib/kward/session_store.rb
|
|
193
|
+
- lib/kward/session_trash.rb
|
|
194
|
+
- lib/kward/session_tree_renderer.rb
|
|
187
195
|
- lib/kward/skills/registry.rb
|
|
188
196
|
- lib/kward/starter_pack_installer.rb
|
|
189
197
|
- lib/kward/steering.rb
|