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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +90 -0
  6. data/LICENSE +21 -0
  7. data/README.md +101 -0
  8. data/Rakefile +20 -0
  9. data/doc/authentication.md +105 -0
  10. data/doc/code-search.md +56 -0
  11. data/doc/configuration.md +310 -0
  12. data/doc/extensibility.md +186 -0
  13. data/doc/getting-started.md +127 -0
  14. data/doc/memory.md +192 -0
  15. data/doc/plugins.md +223 -0
  16. data/doc/releasing.md +36 -0
  17. data/doc/rpc.md +635 -0
  18. data/doc/usage.md +179 -0
  19. data/doc/web-search.md +28 -0
  20. data/exe/kward +5 -0
  21. data/kward.gemspec +33 -0
  22. data/lib/kward/agent.rb +234 -0
  23. data/lib/kward/ansi.rb +276 -0
  24. data/lib/kward/auth/file.rb +11 -0
  25. data/lib/kward/auth/github_oauth.rb +222 -0
  26. data/lib/kward/auth/openai_oauth.rb +323 -0
  27. data/lib/kward/auth/openrouter_api_key.rb +40 -0
  28. data/lib/kward/cancellation.rb +54 -0
  29. data/lib/kward/cli.rb +2122 -0
  30. data/lib/kward/clipboard.rb +84 -0
  31. data/lib/kward/compactor.rb +998 -0
  32. data/lib/kward/config_files.rb +564 -0
  33. data/lib/kward/conversation.rb +148 -0
  34. data/lib/kward/events.rb +13 -0
  35. data/lib/kward/export_path.rb +28 -0
  36. data/lib/kward/image_attachments.rb +331 -0
  37. data/lib/kward/markdown_transcript.rb +72 -0
  38. data/lib/kward/memory/manager.rb +652 -0
  39. data/lib/kward/message_access.rb +42 -0
  40. data/lib/kward/model/chat_invocation.rb +23 -0
  41. data/lib/kward/model/client.rb +875 -0
  42. data/lib/kward/model/context_overflow.rb +55 -0
  43. data/lib/kward/model/context_usage.rb +104 -0
  44. data/lib/kward/model/model_info.rb +188 -0
  45. data/lib/kward/model/retry_message.rb +11 -0
  46. data/lib/kward/model/stream_parser.rb +205 -0
  47. data/lib/kward/pan/index.html.erb +143 -0
  48. data/lib/kward/pan/server.rb +397 -0
  49. data/lib/kward/plugin_registry.rb +327 -0
  50. data/lib/kward/private_file.rb +18 -0
  51. data/lib/kward/prompt_interface.rb +2437 -0
  52. data/lib/kward/prompts/commands.rb +50 -0
  53. data/lib/kward/prompts/templates.rb +60 -0
  54. data/lib/kward/prompts.rb +58 -0
  55. data/lib/kward/resources/avatar_kward_logo.rb +48 -0
  56. data/lib/kward/resources/pixel_logo.rb +230 -0
  57. data/lib/kward/rpc/auth_manager.rb +265 -0
  58. data/lib/kward/rpc/config_manager.rb +58 -0
  59. data/lib/kward/rpc/prompt_bridge.rb +104 -0
  60. data/lib/kward/rpc/redactor.rb +47 -0
  61. data/lib/kward/rpc/server.rb +639 -0
  62. data/lib/kward/rpc/session_manager.rb +1122 -0
  63. data/lib/kward/rpc/tool_event_normalizer.rb +68 -0
  64. data/lib/kward/rpc/tool_metadata.rb +80 -0
  65. data/lib/kward/rpc/transcript_normalizer.rb +307 -0
  66. data/lib/kward/rpc/transport.rb +58 -0
  67. data/lib/kward/session_diff.rb +125 -0
  68. data/lib/kward/session_store.rb +493 -0
  69. data/lib/kward/skills/registry.rb +76 -0
  70. data/lib/kward/starter_pack_installer.rb +110 -0
  71. data/lib/kward/steering.rb +56 -0
  72. data/lib/kward/telemetry/logger.rb +195 -0
  73. data/lib/kward/telemetry/stats.rb +466 -0
  74. data/lib/kward/tools/ask_user_question.rb +107 -0
  75. data/lib/kward/tools/base.rb +45 -0
  76. data/lib/kward/tools/code_search.rb +65 -0
  77. data/lib/kward/tools/edit_file.rb +41 -0
  78. data/lib/kward/tools/list_directory.rb +21 -0
  79. data/lib/kward/tools/read_file.rb +30 -0
  80. data/lib/kward/tools/read_skill.rb +27 -0
  81. data/lib/kward/tools/registry.rb +117 -0
  82. data/lib/kward/tools/run_shell_command.rb +28 -0
  83. data/lib/kward/tools/search/code.rb +445 -0
  84. data/lib/kward/tools/search/web.rb +747 -0
  85. data/lib/kward/tools/tool_call.rb +87 -0
  86. data/lib/kward/tools/web_search.rb +48 -0
  87. data/lib/kward/tools/write_file.rb +29 -0
  88. data/lib/kward/transcript_export.rb +40 -0
  89. data/lib/kward/version.rb +4 -0
  90. data/lib/kward/workspace.rb +377 -0
  91. data/lib/kward.rb +6 -0
  92. data/lib/main.rb +3 -0
  93. 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