kward 0.84.0 → 0.85.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +2 -2
  5. data/doc/agent-tools.md +13 -1
  6. data/doc/api.md +17 -2
  7. data/doc/composer.md +1 -1
  8. data/doc/configuration.md +27 -4
  9. data/doc/extensibility.md +2 -1
  10. data/doc/git.md +3 -1
  11. data/doc/pan.md +6 -0
  12. data/doc/permissions.md +4 -4
  13. data/doc/plugins.md +464 -15
  14. data/doc/rpc.md +154 -16
  15. data/doc/sandboxing.md +11 -5
  16. data/doc/security.md +7 -1
  17. data/doc/session-management.md +5 -4
  18. data/doc/tabs.md +6 -2
  19. data/doc/transports.md +15 -0
  20. data/doc/usage.md +4 -1
  21. data/doc/workspace-tools.md +9 -0
  22. data/examples/plugins/space_invaders.rb +1 -1
  23. data/examples/plugins/stardate_footer.rb +2 -2
  24. data/examples/plugins/telegram/plugin.rb +1 -1
  25. data/lib/kward/agent.rb +24 -11
  26. data/lib/kward/cli/compaction.rb +9 -3
  27. data/lib/kward/cli/plugins.rb +81 -12
  28. data/lib/kward/cli/prompt_interface.rb +25 -5
  29. data/lib/kward/cli/rendering.rb +3 -0
  30. data/lib/kward/cli/runtime_helpers.rb +121 -27
  31. data/lib/kward/cli/sessions.rb +9 -5
  32. data/lib/kward/cli/settings/menus.rb +745 -0
  33. data/lib/kward/cli/settings/model.rb +327 -0
  34. data/lib/kward/cli/settings.rb +6 -1055
  35. data/lib/kward/cli/slash_commands.rb +45 -4
  36. data/lib/kward/cli/tabs.rb +167 -27
  37. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
  38. data/lib/kward/cli/worktrees.rb +65 -2
  39. data/lib/kward/cli.rb +26 -24
  40. data/lib/kward/compactor.rb +18 -7
  41. data/lib/kward/config/core.rb +389 -0
  42. data/lib/kward/config/extensions.rb +96 -0
  43. data/lib/kward/config/prompts.rb +313 -0
  44. data/lib/kward/config/settings.rb +250 -0
  45. data/lib/kward/config_files.rb +14 -1008
  46. data/lib/kward/conversation.rb +31 -2
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +2 -2
  49. data/lib/kward/model/copilot_models.rb +2 -2
  50. data/lib/kward/model/model_info.rb +20 -3
  51. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  52. data/lib/kward/model/payloads.rb +12 -3
  53. data/lib/kward/model/typesafe_client.rb +78 -0
  54. data/lib/kward/pan/server.rb +10 -7
  55. data/lib/kward/permissions/policy.rb +6 -2
  56. data/lib/kward/plugin_registry.rb +2 -659
  57. data/lib/kward/plugins/actions.rb +453 -0
  58. data/lib/kward/plugins/chat_contract.rb +121 -0
  59. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  60. data/lib/kward/plugins/host.rb +232 -0
  61. data/lib/kward/plugins/registry.rb +1190 -0
  62. data/lib/kward/plugins/resources.rb +206 -0
  63. data/lib/kward/plugins/turn_request.rb +36 -0
  64. data/lib/kward/plugins/ui.rb +219 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
  66. data/lib/kward/prompt_interface/composer_state.rb +1 -1
  67. data/lib/kward/prompt_interface/editor/controller.rb +1 -1
  68. data/lib/kward/prompt_interface/editor/runner.rb +1 -1
  69. data/lib/kward/prompt_interface/editor/state.rb +1 -1
  70. data/lib/kward/prompt_interface/layout.rb +1 -1
  71. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
  72. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  73. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  74. data/lib/kward/prompt_interface/screen.rb +9 -2
  75. data/lib/kward/prompt_interface.rb +60 -11
  76. data/lib/kward/prompts/commands.rb +2 -1
  77. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  78. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
  79. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  80. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  81. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
  82. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  83. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  84. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  85. data/lib/kward/rpc/server.rb +91 -12
  86. data/lib/kward/rpc/session_manager.rb +147 -44
  87. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  88. data/lib/kward/rpc/tool_metadata.rb +1 -1
  89. data/lib/kward/sandbox/command_runner.rb +1 -1
  90. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  91. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  92. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  93. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  94. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  95. data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
  96. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
  97. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  98. data/lib/kward/skills/trust_store.rb +1 -1
  99. data/lib/kward/tabs/driver.rb +194 -0
  100. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  101. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  102. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  103. data/lib/kward/tools/base.rb +18 -0
  104. data/lib/kward/tools/context_for_task.rb +15 -6
  105. data/lib/kward/tools/edit_file.rb +9 -6
  106. data/lib/kward/tools/git_commit.rb +13 -7
  107. data/lib/kward/tools/list_directory.rb +4 -4
  108. data/lib/kward/tools/plugin_tool.rb +41 -0
  109. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  110. data/lib/kward/tools/read_file.rb +7 -6
  111. data/lib/kward/tools/registry.rb +96 -13
  112. data/lib/kward/tools/run_shell_command.rb +10 -8
  113. data/lib/kward/tools/search/code.rb +1 -1
  114. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  115. data/lib/kward/tools/tool_call.rb +2 -1
  116. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  117. data/lib/kward/tools/workspace_targets.rb +58 -0
  118. data/lib/kward/tools/write_file.rb +9 -6
  119. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  120. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  121. data/lib/kward/transport/contracts.rb +200 -0
  122. data/lib/kward/transport/gateway.rb +79 -35
  123. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  124. data/lib/kward/transport.rb +1 -200
  125. data/lib/kward/version.rb +1 -1
  126. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  127. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  128. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  129. data/templates/default/fulldoc/html/css/kward.css +125 -0
  130. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  131. data/templates/default/fulldoc/html/setup.rb +1 -1
  132. data/templates/default/layout/html/layout.erb +16 -4
  133. metadata +67 -48
  134. data/lib/kward/tab_driver.rb +0 -90
  135. data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
  136. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  137. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  138. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  139. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  140. /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
  141. /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
  142. /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
  143. /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
  144. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  145. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  146. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  147. /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
  148. /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
  149. /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
  150. /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
  151. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  152. /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
  153. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  154. /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
  155. /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
@@ -0,0 +1,194 @@
1
+ require "digest"
2
+ require "logger"
3
+ require_relative "../config_files"
4
+ require_relative "../deep_copy"
5
+ require_relative "../plugins/chat_contract"
6
+ require_relative "../plugins/host"
7
+ require_relative "../plugins/resources"
8
+
9
+ # Namespace for the Kward CLI agent runtime.
10
+ module Kward
11
+ # Adapts a session-backed agent to the tab runtime interface. Plugin tab
12
+ # drivers implement the same small surface without becoming Kward sessions.
13
+ class SessionTabDriver
14
+ attr_reader :session, :agent, :worktree
15
+
16
+ def initialize(session:, agent:, worktree: nil)
17
+ @session = session
18
+ @agent = agent
19
+ @worktree = worktree
20
+ end
21
+
22
+ def messages
23
+ agent.conversation.messages
24
+ end
25
+
26
+ def conversation
27
+ agent.conversation
28
+ end
29
+
30
+ def submit(input, display_input:, cancellation:, steering: nil, &block)
31
+ options = { cancellation: cancellation }
32
+ options[:display_input] = display_input unless display_input.nil?
33
+ options[:steering] = steering if steering
34
+ agent.ask(input, **options, &block)
35
+ end
36
+
37
+ def descriptor
38
+ descriptor = { "kind" => "session", "session_path" => session.path }
39
+ descriptor["worktree"] = worktree.descriptor if worktree
40
+ descriptor
41
+ end
42
+
43
+ def session?
44
+ true
45
+ end
46
+
47
+ def supports_steering?
48
+ true
49
+ end
50
+
51
+ def assistant_label
52
+ nil
53
+ end
54
+ end
55
+
56
+ # Represents a persisted plugin tab whose provider plugin is unavailable.
57
+ # Its descriptor is retained so reinstalling the plugin restores the tab.
58
+ class UnavailableTabDriver
59
+ attr_reader :descriptor
60
+
61
+ def initialize(descriptor:, message:)
62
+ @descriptor = descriptor
63
+ @message = message
64
+ end
65
+
66
+ def messages
67
+ [{ role: "assistant", content: @message }]
68
+ end
69
+
70
+ def submit(*)
71
+ raise @message
72
+ end
73
+
74
+ def session?
75
+ false
76
+ end
77
+
78
+ def supports_steering?
79
+ false
80
+ end
81
+
82
+ def assistant_label
83
+ "Plugin"
84
+ end
85
+ end
86
+
87
+ # Dependencies and instance-scoped services made available to a plugin chat
88
+ # factory without exposing CLI or workspace-session internals.
89
+ class PluginTabHost
90
+ SURFACES = %i[local rpc transport shared].freeze
91
+
92
+ class LogDevice
93
+ def write(message)
94
+ ConfigFiles.emit_warning(message.to_s.chomp)
95
+ end
96
+
97
+ def close
98
+ nil
99
+ end
100
+ end
101
+ private_constant :LogDevice
102
+
103
+ attr_reader :client, :workspace_root, :plugin_id, :type_id, :surface, :scope_key,
104
+ :capabilities, :config, :storage, :logger
105
+
106
+ def initialize(client:, workspace_root:, plugin_host: nil, type_id: nil, surface: :local, scope_key: "default", capabilities: nil, config: nil, storage: nil, logger: nil, env: ENV)
107
+ @client = client
108
+ @workspace_root = workspace_root
109
+ @plugin_id = plugin_host&.id
110
+ @type_id = required_value(type_id || @plugin_id || "anonymous", "plugin chat type id").freeze
111
+ @surface = normalize_surface(surface)
112
+ @scope_key = required_value(scope_key, "plugin chat scope key").freeze
113
+ @capabilities = capabilities || PluginChatCapabilities.legacy
114
+ @config = freeze_config(config || plugin_host&.config || ConfigFiles.plugin_config(storage_owner_id))
115
+ @storage = PluginScopedStore.new(storage || plugin_host&.storage || default_storage, storage_namespace)
116
+ @logger = logger || plugin_host&.logger || default_logger
117
+ @env = env
118
+ @resources = PluginResources.new(name: "Kward plugin chat #{@type_id}", warning_sink: ConfigFiles.warning_sink).tap(&:activate!)
119
+ end
120
+
121
+ # Starts cooperative background work owned by this chat instance.
122
+ def background(name: nil, cancellation: nil, &block)
123
+ @resources.background(name: name, cancellation: cancellation, &block)
124
+ end
125
+
126
+ # Registers cleanup that runs when the chat instance closes.
127
+ def on_cleanup(resource = nil, &block)
128
+ @resources.on_cleanup(resource, &block)
129
+ end
130
+
131
+ alias manage on_cleanup
132
+
133
+ # Reads a secret from private plugin config, an explicit environment
134
+ # variable, or the plugin's conventional KWARD_PLUGIN_* variable.
135
+ def secret(key, env: nil)
136
+ key = required_value(key, "secret key")
137
+ value = config[key]
138
+ value = @env[env.to_s] if value.nil? && env
139
+ value = @env[default_secret_env_name(key)] if value.nil?
140
+ value.to_s unless value.nil?
141
+ end
142
+
143
+ # Cancels chat-owned work and invokes cleanup callbacks.
144
+ # @api private
145
+ def shutdown(timeout: PluginResources::DEFAULT_SHUTDOWN_TIMEOUT)
146
+ @resources.shutdown(timeout: timeout)
147
+ self
148
+ end
149
+
150
+ private
151
+
152
+ def storage_owner_id
153
+ plugin_id || type_id
154
+ end
155
+
156
+ def storage_namespace
157
+ "plugin_chat:#{type_id}:#{scope_key}"
158
+ end
159
+
160
+ def default_storage
161
+ safe_id = "chat.#{Digest::SHA256.hexdigest(storage_owner_id)[0, 24]}"
162
+ PluginStore.new(safe_id)
163
+ end
164
+
165
+ def freeze_config(value)
166
+ raise ArgumentError, "Kward plugin config for #{storage_owner_id} must be an object" unless value.is_a?(Hash)
167
+
168
+ DeepCopy.freeze(DeepCopy.dup(value))
169
+ end
170
+
171
+ def normalize_surface(value)
172
+ surface = value.to_sym
173
+ raise ArgumentError, "Unknown plugin chat surface: #{value}" unless SURFACES.include?(surface)
174
+
175
+ surface
176
+ end
177
+
178
+ def required_value(value, name)
179
+ value = value.to_s
180
+ raise ArgumentError, "#{name} is required" if value.empty?
181
+
182
+ value
183
+ end
184
+
185
+ def default_secret_env_name(key)
186
+ parts = [storage_owner_id, key].map { |part| part.gsub(/[^A-Za-z0-9]/, "_").upcase }
187
+ "KWARD_PLUGIN_#{parts.join("_")}"
188
+ end
189
+
190
+ def default_logger
191
+ Logger.new(LogDevice.new).tap { |logger| logger.progname = "Kward plugin chat #{type_id}" }
192
+ end
193
+ end
194
+ end
@@ -1,7 +1,7 @@
1
1
  require "digest"
2
2
  require "json"
3
- require_relative "config_files"
4
- require_relative "private_file"
3
+ require_relative "../config_files"
4
+ require_relative "../private_file"
5
5
 
6
6
  # Namespace for the Kward CLI agent runtime.
7
7
  module Kward
@@ -1,5 +1,5 @@
1
1
  require "open3"
2
- require_relative "terminal_sequences"
2
+ require_relative "sequences"
3
3
  require "rbconfig"
4
4
 
5
5
  # Namespace for the Kward CLI agent runtime.
@@ -1,4 +1,4 @@
1
- require_relative "terminal_sequences"
1
+ require_relative "sequences"
2
2
 
3
3
  # Namespace for the Kward CLI agent runtime.
4
4
  module Kward
@@ -1,3 +1,5 @@
1
+ require_relative "workspace_targets"
2
+
1
3
  # Namespace for the Kward CLI agent runtime.
2
4
  module Kward
3
5
  # Model-callable tool wrappers and their argument schemas.
@@ -54,6 +56,22 @@ module Kward
54
56
  end
55
57
  end
56
58
 
59
+ # Configures the active workspace and any additional host-scoped targets.
60
+ def configure_workspace_targets(workspace, workspace_targets)
61
+ @workspace = workspace
62
+ @workspace_targets = workspace_targets || WorkspaceTargets.new(active: workspace)
63
+ end
64
+
65
+ # Adds the optional target selector when additional workspaces are available.
66
+ def targeted_properties(properties)
67
+ @workspace_targets.properties(properties)
68
+ end
69
+
70
+ # Resolves the requested role through the trusted target map.
71
+ def workspace_for(args)
72
+ @workspace_targets.workspace_for(args)
73
+ end
74
+
57
75
  # Reads a tool argument while accepting symbol or string keys from restored calls.
58
76
  def argument(args, key, default = nil)
59
77
  return args[key] if args.key?(key)
@@ -1,5 +1,5 @@
1
1
  require "pathname"
2
- require_relative "../workspace"
2
+ require_relative "../workspace/workspace"
3
3
  require_relative "base"
4
4
 
5
5
  # Namespace for the Kward CLI agent runtime.
@@ -16,23 +16,32 @@ module Kward
16
16
  SKIP_DIRECTORIES = %w[.git .yardoc _yardoc node_modules vendor tmp log coverage dist build .bundle].freeze
17
17
 
18
18
  # Builds the tool schema and stores the execution dependency.
19
- def initialize(workspace:)
20
- @workspace = workspace
19
+ def initialize(workspace:, workspace_targets: nil)
20
+ configure_workspace_targets(workspace, workspace_targets)
21
21
  super(
22
22
  "context_for_task",
23
23
  "Build focused workspace context for a task from outlines and matching excerpts within a byte budget.",
24
- properties: {
24
+ properties: targeted_properties(
25
25
  budget: { type: "integer", description: "Approximate byte budget for the returned context. Default 4000, maximum 20000." },
26
26
  paths: { type: "array", items: { type: "string" }, description: "Optional workspace-relative files or directories to focus." },
27
27
  task: { type: "string", description: "Task or question to gather context for." }
28
- },
28
+ ),
29
29
  required: ["task"]
30
30
  )
31
31
  end
32
32
 
33
33
  # Executes focused context retrieval.
34
- def call(args, _conversation, cancellation: nil)
34
+ def call(args, conversation, cancellation: nil)
35
35
  cancellation&.raise_if_cancelled!
36
+ selected_workspace = workspace_for(args)
37
+ unless selected_workspace.equal?(@workspace)
38
+ return self.class.new(workspace: selected_workspace).call(
39
+ @workspace_targets.without_target(args),
40
+ conversation,
41
+ cancellation: cancellation
42
+ )
43
+ end
44
+
36
45
  task = argument(args, :task, "").to_s.strip
37
46
  return "Error: task is required" if task.empty?
38
47
 
@@ -7,12 +7,12 @@ module Kward
7
7
  # Tool wrapper for exact block replacement edits.
8
8
  class EditFile < Base
9
9
  # Builds the tool schema and stores the execution dependency.
10
- def initialize(workspace:)
11
- @workspace = workspace
10
+ def initialize(workspace:, workspace_targets: nil)
11
+ configure_workspace_targets(workspace, workspace_targets)
12
12
  super(
13
13
  "edit_file",
14
14
  "Edit a read workspace file by exact replacements. Each old_text must match once; edits must not overlap.",
15
- properties: {
15
+ properties: targeted_properties(
16
16
  path: { type: "string", description: "Workspace-relative path." },
17
17
  edits: {
18
18
  type: "array",
@@ -27,7 +27,7 @@ module Kward
27
27
  additionalProperties: false
28
28
  }
29
29
  }
30
- },
30
+ ),
31
31
  required: ["path", "edits"]
32
32
  )
33
33
  end
@@ -37,8 +37,11 @@ module Kward
37
37
  path = argument(args, :path, "")
38
38
  edits = argument(args, :edits, [])
39
39
 
40
- result = @workspace.edit_file(path, edits, read_paths: conversation.read_paths)
41
- conversation.refresh_system_message! if agents_file_changed?(@workspace, path, result)
40
+ workspace = workspace_for(args)
41
+ result = workspace.edit_file(path, edits, read_paths: conversation.read_paths)
42
+ if workspace.equal?(@workspace) && agents_file_changed?(workspace, path, result)
43
+ conversation.refresh_system_message!
44
+ end
42
45
  result
43
46
  end
44
47
  end
@@ -5,15 +5,18 @@ module Kward
5
5
  module Tools
6
6
  # Tool wrapper for the trusted host-side Git commit operation.
7
7
  class GitCommit < Base
8
- def initialize(committer:)
8
+ def initialize(committer:, workspace_targets: nil)
9
9
  @committer = committer
10
+ @workspace_targets = workspace_targets
11
+ properties = {
12
+ message: { type: "string", description: "Commit message." },
13
+ paths: { type: "array", items: { type: "string" }, description: "Optional workspace-relative paths to include." }
14
+ }
15
+ properties = workspace_targets.properties(properties) if workspace_targets
10
16
  super(
11
17
  "git_commit",
12
- "Stage and commit changes in the active worktree. Omit paths to include all current changes.",
13
- properties: {
14
- message: { type: "string", description: "Commit message." },
15
- paths: { type: "array", items: { type: "string" }, description: "Optional workspace-relative paths to include." }
16
- },
18
+ "Stage and commit changes in the selected worktree (active by default). Omit paths to include all current changes.",
19
+ properties: properties,
17
20
  required: ["message"]
18
21
  )
19
22
  end
@@ -27,7 +30,10 @@ module Kward
27
30
  return "Error: paths must contain strings" if paths && paths.any? { |path| !path.is_a?(String) }
28
31
 
29
32
  paths = nil if paths&.empty?
30
- result = @committer.call(message: message, paths: paths)
33
+ target = @workspace_targets&.target_name(args)
34
+ options = { message: message, paths: paths }
35
+ options[:target] = target if target && target != WorkspaceTargets::ACTIVE
36
+ result = @committer.call(**options)
31
37
  output = result[:output].to_s.strip
32
38
  return "Git commit succeeded\n#{output}" if result[:success]
33
39
 
@@ -7,19 +7,19 @@ module Kward
7
7
  # Tool wrapper for listing workspace directory entries.
8
8
  class ListDirectory < Base
9
9
  # Builds the tool schema and stores the execution dependency.
10
- def initialize(workspace:)
11
- @workspace = workspace
10
+ def initialize(workspace:, workspace_targets: nil)
11
+ configure_workspace_targets(workspace, workspace_targets)
12
12
  super(
13
13
  "list_directory",
14
14
  "List workspace directory entries.",
15
- properties: { path: { type: "string", description: "Workspace-relative directory." } },
15
+ properties: targeted_properties(path: { type: "string", description: "Workspace-relative directory." }),
16
16
  required: ["path"]
17
17
  )
18
18
  end
19
19
 
20
20
  # Executes the tool and returns model-facing output text.
21
21
  def call(args, _conversation, cancellation: nil)
22
- @workspace.list_directory(argument(args, :path, "."))
22
+ workspace_for(args).list_directory(argument(args, :path, "."))
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,41 @@
1
+ require_relative "base"
2
+
3
+ # Namespace for the Kward CLI agent runtime.
4
+ module Kward
5
+ # Model-callable tool wrappers and their argument schemas.
6
+ module Tools
7
+ # Adapts a trusted plugin tool registration to the normal tool interface.
8
+ class PluginTool < Base
9
+ attr_reader :plugin_path
10
+
11
+ def initialize(registration:, context_factory:)
12
+ @description = registration.description
13
+ @parameters = registration.schema
14
+ @plugin_path = registration.path
15
+ @handler = registration.handler
16
+ @context_factory = context_factory
17
+ super(registration.name, registration.description)
18
+ end
19
+
20
+ # Returns the strict JSON schema declared by the plugin.
21
+ def schema
22
+ {
23
+ type: "function",
24
+ function: {
25
+ name: name,
26
+ description: @description,
27
+ parameters: @parameters
28
+ }
29
+ }
30
+ end
31
+
32
+ # Executes trusted plugin code with parsed model arguments and scoped context.
33
+ def call(args, conversation, cancellation: nil)
34
+ context = @context_factory.call(conversation, cancellation)
35
+ result = @handler.call(args, context)
36
+ cancellation&.raise_if_cancelled!
37
+ result.to_s
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  require_relative "base"
2
- require_relative "../shell_prompt_session"
2
+ require_relative "../shell/prompt_session"
3
3
 
4
4
  # Namespace for the Kward agent runtime.
5
5
  module Kward
@@ -7,18 +7,18 @@ module Kward
7
7
  # Tool wrapper for bounded workspace file reads.
8
8
  class ReadFile < Base
9
9
  # Builds the tool schema and stores the execution dependency.
10
- def initialize(workspace:)
11
- @workspace = workspace
10
+ def initialize(workspace:, workspace_targets: nil)
11
+ configure_workspace_targets(workspace, workspace_targets)
12
12
  super(
13
13
  "read_file",
14
14
  "Read a workspace text file. Output is capped; use offset/limit or mode to control context budget.",
15
- properties: {
15
+ properties: targeted_properties(
16
16
  path: { type: "string", description: "Workspace-relative path." },
17
17
  limit: { type: "integer", description: "Maximum lines to return." },
18
18
  max_bytes: { type: "integer", description: "Optional byte budget for this read, capped by Kward's workspace read limit." },
19
19
  mode: { type: "string", enum: %w[preview outline range full], description: "Context mode. preview returns a short slice, outline returns source declarations, range reads offset/limit, full reads until Kward's cap." },
20
20
  offset: { type: "integer", description: "1-indexed start line." }
21
- },
21
+ ),
22
22
  required: ["path"]
23
23
  )
24
24
  end
@@ -30,8 +30,9 @@ module Kward
30
30
  limit = argument(args, :limit)
31
31
  mode = argument(args, :mode)
32
32
  max_bytes = argument(args, :max_bytes)
33
- content = @workspace.read_file(path, offset: offset, limit: limit, mode: mode, max_bytes: max_bytes)
34
- conversation.mark_read(@workspace.resolved_path(path)) unless content.start_with?("Error:")
33
+ workspace = workspace_for(args)
34
+ content = workspace.read_file(path, offset: offset, limit: limit, mode: mode, max_bytes: max_bytes)
35
+ conversation.mark_read(workspace.resolved_path(path)) unless content.start_with?("Error:")
35
36
  content
36
37
  end
37
38
  end