kward 0.66.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 +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +90 -0
- data/LICENSE +21 -0
- data/README.md +101 -0
- data/Rakefile +20 -0
- data/doc/authentication.md +105 -0
- data/doc/code-search.md +56 -0
- data/doc/configuration.md +310 -0
- data/doc/extensibility.md +186 -0
- data/doc/getting-started.md +127 -0
- data/doc/memory.md +192 -0
- data/doc/plugins.md +223 -0
- data/doc/releasing.md +36 -0
- data/doc/rpc.md +635 -0
- data/doc/usage.md +179 -0
- data/doc/web-search.md +28 -0
- data/exe/kward +5 -0
- data/kward.gemspec +33 -0
- data/lib/kward/agent.rb +234 -0
- data/lib/kward/ansi.rb +276 -0
- data/lib/kward/auth/file.rb +11 -0
- data/lib/kward/auth/github_oauth.rb +222 -0
- data/lib/kward/auth/openai_oauth.rb +323 -0
- data/lib/kward/auth/openrouter_api_key.rb +40 -0
- data/lib/kward/cancellation.rb +54 -0
- data/lib/kward/cli.rb +2122 -0
- data/lib/kward/clipboard.rb +84 -0
- data/lib/kward/compactor.rb +998 -0
- data/lib/kward/config_files.rb +564 -0
- data/lib/kward/conversation.rb +148 -0
- data/lib/kward/events.rb +13 -0
- data/lib/kward/export_path.rb +28 -0
- data/lib/kward/image_attachments.rb +331 -0
- data/lib/kward/markdown_transcript.rb +72 -0
- data/lib/kward/memory/manager.rb +652 -0
- data/lib/kward/message_access.rb +42 -0
- data/lib/kward/model/chat_invocation.rb +23 -0
- data/lib/kward/model/client.rb +875 -0
- data/lib/kward/model/context_overflow.rb +55 -0
- data/lib/kward/model/context_usage.rb +104 -0
- data/lib/kward/model/model_info.rb +188 -0
- data/lib/kward/model/retry_message.rb +11 -0
- data/lib/kward/model/stream_parser.rb +205 -0
- data/lib/kward/pan/index.html.erb +143 -0
- data/lib/kward/pan/server.rb +397 -0
- data/lib/kward/plugin_registry.rb +327 -0
- data/lib/kward/private_file.rb +18 -0
- data/lib/kward/prompt_interface.rb +2437 -0
- data/lib/kward/prompts/commands.rb +50 -0
- data/lib/kward/prompts/templates.rb +60 -0
- data/lib/kward/prompts.rb +58 -0
- data/lib/kward/resources/avatar_kward_logo.rb +48 -0
- data/lib/kward/resources/pixel_logo.rb +230 -0
- data/lib/kward/rpc/auth_manager.rb +265 -0
- data/lib/kward/rpc/config_manager.rb +58 -0
- data/lib/kward/rpc/prompt_bridge.rb +104 -0
- data/lib/kward/rpc/redactor.rb +47 -0
- data/lib/kward/rpc/server.rb +639 -0
- data/lib/kward/rpc/session_manager.rb +1122 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +68 -0
- data/lib/kward/rpc/tool_metadata.rb +80 -0
- data/lib/kward/rpc/transcript_normalizer.rb +307 -0
- data/lib/kward/rpc/transport.rb +58 -0
- data/lib/kward/session_diff.rb +125 -0
- data/lib/kward/session_store.rb +493 -0
- data/lib/kward/skills/registry.rb +76 -0
- data/lib/kward/starter_pack_installer.rb +110 -0
- data/lib/kward/steering.rb +56 -0
- data/lib/kward/telemetry/logger.rb +195 -0
- data/lib/kward/telemetry/stats.rb +466 -0
- data/lib/kward/tools/ask_user_question.rb +107 -0
- data/lib/kward/tools/base.rb +45 -0
- data/lib/kward/tools/code_search.rb +65 -0
- data/lib/kward/tools/edit_file.rb +41 -0
- data/lib/kward/tools/list_directory.rb +21 -0
- data/lib/kward/tools/read_file.rb +30 -0
- data/lib/kward/tools/read_skill.rb +27 -0
- data/lib/kward/tools/registry.rb +117 -0
- data/lib/kward/tools/run_shell_command.rb +28 -0
- data/lib/kward/tools/search/code.rb +445 -0
- data/lib/kward/tools/search/web.rb +747 -0
- data/lib/kward/tools/tool_call.rb +87 -0
- data/lib/kward/tools/web_search.rb +48 -0
- data/lib/kward/tools/write_file.rb +29 -0
- data/lib/kward/transcript_export.rb +40 -0
- data/lib/kward/version.rb +4 -0
- data/lib/kward/workspace.rb +377 -0
- data/lib/kward.rb +6 -0
- data/lib/main.rb +3 -0
- metadata +232 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module Kward
|
|
4
|
+
module Tools
|
|
5
|
+
class EditFile < Base
|
|
6
|
+
def initialize(workspace:)
|
|
7
|
+
@workspace = workspace
|
|
8
|
+
super(
|
|
9
|
+
"edit_file",
|
|
10
|
+
"Edit a read workspace file by exact replacements. Each old_text must match once; edits must not overlap.",
|
|
11
|
+
properties: {
|
|
12
|
+
path: { type: "string", description: "Workspace-relative path." },
|
|
13
|
+
edits: {
|
|
14
|
+
type: "array",
|
|
15
|
+
description: "Non-overlapping replacements against original content.",
|
|
16
|
+
items: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
old_text: { type: "string", description: "Unique exact text to replace." },
|
|
20
|
+
new_text: { type: "string", description: "Replacement text." }
|
|
21
|
+
},
|
|
22
|
+
required: ["old_text", "new_text"],
|
|
23
|
+
additionalProperties: false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: ["path", "edits"]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(args, conversation, cancellation: nil)
|
|
32
|
+
path = argument(args, :path, "")
|
|
33
|
+
edits = argument(args, :edits, [])
|
|
34
|
+
|
|
35
|
+
result = @workspace.edit_file(path, edits, read_paths: conversation.read_paths)
|
|
36
|
+
conversation.refresh_system_message! if agents_file_changed?(@workspace, path, result)
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module Kward
|
|
4
|
+
module Tools
|
|
5
|
+
class ListDirectory < Base
|
|
6
|
+
def initialize(workspace:)
|
|
7
|
+
@workspace = workspace
|
|
8
|
+
super(
|
|
9
|
+
"list_directory",
|
|
10
|
+
"List workspace directory entries.",
|
|
11
|
+
properties: { path: { type: "string", description: "Workspace-relative directory." } },
|
|
12
|
+
required: ["path"]
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(args, _conversation, cancellation: nil)
|
|
17
|
+
@workspace.list_directory(argument(args, :path, "."))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module Kward
|
|
4
|
+
module Tools
|
|
5
|
+
class ReadFile < Base
|
|
6
|
+
def initialize(workspace:)
|
|
7
|
+
@workspace = workspace
|
|
8
|
+
super(
|
|
9
|
+
"read_file",
|
|
10
|
+
"Read a workspace text file. Output is capped; use offset/limit to continue.",
|
|
11
|
+
properties: {
|
|
12
|
+
path: { type: "string", description: "Workspace-relative path." },
|
|
13
|
+
offset: { type: "integer", description: "1-indexed start line." },
|
|
14
|
+
limit: { type: "integer", description: "Maximum lines to return." }
|
|
15
|
+
},
|
|
16
|
+
required: ["path"]
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(args, conversation, cancellation: nil)
|
|
21
|
+
path = argument(args, :path, "")
|
|
22
|
+
offset = argument(args, :offset)
|
|
23
|
+
limit = argument(args, :limit)
|
|
24
|
+
content = @workspace.read_file(path, offset: offset, limit: limit)
|
|
25
|
+
conversation.mark_read(@workspace.resolved_path(path)) unless content.start_with?("Error:")
|
|
26
|
+
content
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
require_relative "../config_files"
|
|
3
|
+
|
|
4
|
+
module Kward
|
|
5
|
+
module Tools
|
|
6
|
+
class ReadSkill < Base
|
|
7
|
+
def initialize
|
|
8
|
+
super(
|
|
9
|
+
"read_skill",
|
|
10
|
+
"Read configured skill instructions/files.",
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: "string", description: "Skill name." },
|
|
13
|
+
path: { type: "string", description: "Path inside skill; default SKILL.md." }
|
|
14
|
+
},
|
|
15
|
+
required: ["name"]
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(args, _conversation, cancellation: nil)
|
|
20
|
+
name = argument(args, :name, "")
|
|
21
|
+
path = argument(args, :path)
|
|
22
|
+
|
|
23
|
+
ConfigFiles.read_skill_file(name, path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require_relative "../config_files"
|
|
2
|
+
require_relative "ask_user_question"
|
|
3
|
+
require_relative "code_search"
|
|
4
|
+
require_relative "edit_file"
|
|
5
|
+
require_relative "list_directory"
|
|
6
|
+
require_relative "read_file"
|
|
7
|
+
require_relative "read_skill"
|
|
8
|
+
require_relative "run_shell_command"
|
|
9
|
+
require_relative "web_search"
|
|
10
|
+
require_relative "write_file"
|
|
11
|
+
require_relative "search/code"
|
|
12
|
+
require_relative "search/web"
|
|
13
|
+
require_relative "tool_call"
|
|
14
|
+
require_relative "../workspace"
|
|
15
|
+
|
|
16
|
+
module Kward
|
|
17
|
+
# Exposes local workspace, search, skill, and interaction tools to the model
|
|
18
|
+
# and dispatches approved tool calls into the active conversation.
|
|
19
|
+
class ToolRegistry
|
|
20
|
+
# Tool schemas advertised to the model for the current frontend and config.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<Hash>]
|
|
23
|
+
attr_reader :schemas
|
|
24
|
+
|
|
25
|
+
def initialize(workspace: Workspace.new, prompt: nil, web_search: WebSearch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil)
|
|
26
|
+
@workspace = workspace
|
|
27
|
+
@prompt = prompt
|
|
28
|
+
@web_search = web_search
|
|
29
|
+
@code_search = code_search
|
|
30
|
+
@skills = skills
|
|
31
|
+
@web_search_enabled = web_search_enabled
|
|
32
|
+
@ask_user_question_enabled = ask_user_question_enabled
|
|
33
|
+
@tools = build_tools.freeze
|
|
34
|
+
@schemas = build_schema_tools.map(&:schema).freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Executes a model-requested tool call and appends the result to the
|
|
38
|
+
# conversation transcript.
|
|
39
|
+
#
|
|
40
|
+
# @param tool_call [Hash] model tool call payload
|
|
41
|
+
# @param conversation [Conversation] active conversation
|
|
42
|
+
# @return [String] tool output content appended to the conversation
|
|
43
|
+
def dispatch(tool_call, conversation, cancellation: nil)
|
|
44
|
+
cancellation&.raise_if_cancelled!
|
|
45
|
+
name = ToolCall.name(tool_call)
|
|
46
|
+
args = ToolCall.arguments(tool_call)
|
|
47
|
+
tool = @tools[name]
|
|
48
|
+
|
|
49
|
+
content = if tool
|
|
50
|
+
tool.call(args, conversation, cancellation: cancellation)
|
|
51
|
+
else
|
|
52
|
+
"Unknown tool: #{name}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
conversation.append_tool(
|
|
56
|
+
tool_call_id: tool_call["id"] || tool_call[:id],
|
|
57
|
+
name: name,
|
|
58
|
+
content: content
|
|
59
|
+
)
|
|
60
|
+
conversation.append_tool_execution(tool_call: tool_call, content: content)
|
|
61
|
+
|
|
62
|
+
content
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_tools
|
|
68
|
+
all_tools.to_h { |tool| [tool.name, tool] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_schema_tools
|
|
72
|
+
tools = core_tools
|
|
73
|
+
tools << @tools["web_search"] if web_search_available?
|
|
74
|
+
tools << @tools["read_skill"] if skills_available?
|
|
75
|
+
tools << @tools["ask_user_question"] if ask_user_question_available?
|
|
76
|
+
tools
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def all_tools
|
|
80
|
+
core_tools + [
|
|
81
|
+
Tools::WebSearch.new(web_search: @web_search),
|
|
82
|
+
Tools::ReadSkill.new,
|
|
83
|
+
Tools::AskUserQuestion.new(prompt: @prompt)
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def core_tools
|
|
88
|
+
[
|
|
89
|
+
Tools::ListDirectory.new(workspace: @workspace),
|
|
90
|
+
Tools::ReadFile.new(workspace: @workspace),
|
|
91
|
+
Tools::WriteFile.new(workspace: @workspace),
|
|
92
|
+
Tools::EditFile.new(workspace: @workspace),
|
|
93
|
+
Tools::RunShellCommand.new(workspace: @workspace),
|
|
94
|
+
Tools::CodeSearch.new(code_search: @code_search)
|
|
95
|
+
]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def web_search_available?
|
|
99
|
+
return @web_search_enabled unless @web_search_enabled.nil?
|
|
100
|
+
return @web_search.available? if @web_search.respond_to?(:available?)
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def skills_available?
|
|
106
|
+
skills = @skills.nil? ? ConfigFiles.skills : @skills
|
|
107
|
+
skills.any?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ask_user_question_available?
|
|
111
|
+
return false if @ask_user_question_enabled == false
|
|
112
|
+
|
|
113
|
+
@prompt.respond_to?(:ask_user_question)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
require_relative "../workspace"
|
|
3
|
+
|
|
4
|
+
module Kward
|
|
5
|
+
module Tools
|
|
6
|
+
class RunShellCommand < Base
|
|
7
|
+
def initialize(workspace:)
|
|
8
|
+
@workspace = workspace
|
|
9
|
+
super(
|
|
10
|
+
"run_shell_command",
|
|
11
|
+
"Run a shell command from the workspace root.",
|
|
12
|
+
properties: {
|
|
13
|
+
command: { type: "string", description: "Command to run." },
|
|
14
|
+
timeout_seconds: { type: "integer", description: "Timeout seconds; default 30." }
|
|
15
|
+
},
|
|
16
|
+
required: ["command"]
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(args, _conversation, cancellation: nil)
|
|
21
|
+
command = argument(args, :command, "")
|
|
22
|
+
timeout_seconds = argument(args, :timeout_seconds, Workspace::DEFAULT_COMMAND_TIMEOUT_SECONDS)
|
|
23
|
+
|
|
24
|
+
@workspace.run_shell_command(command, timeout_seconds: timeout_seconds, cancellation: cancellation)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|