kward 0.83.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
+
# Tool wrapper for opening a workspace file in the integrated editor.
|
|
8
|
+
class OpenEditor < Base
|
|
9
|
+
# Builds the tool schema and stores the execution dependencies.
|
|
10
|
+
def initialize(workspace:, prompt:)
|
|
11
|
+
@workspace = workspace
|
|
12
|
+
@prompt = prompt
|
|
13
|
+
super(
|
|
14
|
+
"open_editor",
|
|
15
|
+
"Open a workspace file in Kward's integrated editor for the user. Use this when the user explicitly asks to open or edit a file in the built-in editor. This does not itself modify or save the file; the user controls those actions in the editor.",
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: "string", description: "Workspace-relative path of the file to open." }
|
|
18
|
+
},
|
|
19
|
+
required: ["path"]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Opens the requested file and returns after the editor closes.
|
|
24
|
+
def call(args, _conversation, cancellation: nil)
|
|
25
|
+
cancellation&.raise_if_cancelled!
|
|
26
|
+
return "Error: open_editor requires interactive editor support." unless @prompt.respond_to?(:edit_file)
|
|
27
|
+
|
|
28
|
+
path = argument(args, :path, "").to_s
|
|
29
|
+
return "Error: open_editor requires a file path." if path.empty?
|
|
30
|
+
|
|
31
|
+
resolved_path = @workspace.resolved_path(path)
|
|
32
|
+
return "Error: not a file: #{path}" unless File.file?(resolved_path)
|
|
33
|
+
|
|
34
|
+
opened = @prompt.edit_file(resolved_path, base_dir: @workspace.root, allow_new: false)
|
|
35
|
+
opened ? "Opened #{path} in the integrated editor." : "Error: could not open #{path} in the integrated editor."
|
|
36
|
+
rescue SecurityError, StandardError => e
|
|
37
|
+
"Error: could not open #{path}: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
data/lib/kward/tools/registry.rb
CHANGED
|
@@ -11,14 +11,18 @@ require_relative "fetch_raw"
|
|
|
11
11
|
require_relative "git_commit"
|
|
12
12
|
require_relative "list_directory"
|
|
13
13
|
require_relative "mcp_tool"
|
|
14
|
+
require_relative "plugin_tool"
|
|
14
15
|
require_relative "read_file"
|
|
15
16
|
require_relative "read_skill"
|
|
16
17
|
require_relative "run_shell_command"
|
|
17
18
|
require_relative "prepare_shell_command"
|
|
18
19
|
require_relative "summarize_file_structure"
|
|
20
|
+
require_relative "typesafe_evaluate"
|
|
19
21
|
require_relative "retrieve_tool_output"
|
|
20
22
|
require_relative "web_search"
|
|
23
|
+
require_relative "workspace_targets"
|
|
21
24
|
require_relative "write_file"
|
|
25
|
+
require_relative "open_editor"
|
|
22
26
|
require_relative "replace_editor_buffer"
|
|
23
27
|
require_relative "search/code"
|
|
24
28
|
require_relative "search/web"
|
|
@@ -28,7 +32,7 @@ require_relative "../mcp/server_config"
|
|
|
28
32
|
require_relative "../permissions/policy"
|
|
29
33
|
require_relative "../telemetry/logger"
|
|
30
34
|
require_relative "../tool_output_compactor"
|
|
31
|
-
require_relative "../workspace"
|
|
35
|
+
require_relative "../workspace/workspace"
|
|
32
36
|
|
|
33
37
|
# Namespace for the Kward CLI agent runtime.
|
|
34
38
|
module Kward
|
|
@@ -65,6 +69,30 @@ module Kward
|
|
|
65
69
|
retrieve_tool_output
|
|
66
70
|
].freeze
|
|
67
71
|
|
|
72
|
+
TARGETED_WORKSPACE_TOOLS = %w[
|
|
73
|
+
list_directory
|
|
74
|
+
read_file
|
|
75
|
+
write_file
|
|
76
|
+
edit_file
|
|
77
|
+
run_shell_command
|
|
78
|
+
summarize_file_structure
|
|
79
|
+
context_for_task
|
|
80
|
+
git_commit
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
BUILTIN_TOOL_NAMES = (CORE_TOOL_NAMES + %w[
|
|
84
|
+
git_commit
|
|
85
|
+
web_search
|
|
86
|
+
fetch_content
|
|
87
|
+
fetch_raw
|
|
88
|
+
read_skill
|
|
89
|
+
ask_user_question
|
|
90
|
+
replace_editor_buffer
|
|
91
|
+
open_editor
|
|
92
|
+
prepare_shell_command
|
|
93
|
+
typesafe_evaluate
|
|
94
|
+
]).freeze
|
|
95
|
+
|
|
68
96
|
# Tool schemas advertised to the model for the current frontend and config.
|
|
69
97
|
#
|
|
70
98
|
# @return [Array<Hash>] tool schemas currently advertised to the model
|
|
@@ -73,20 +101,24 @@ module Kward
|
|
|
73
101
|
# Builds tool objects and the schema list for the current frontend/config.
|
|
74
102
|
#
|
|
75
103
|
# @param workspace [Workspace] filesystem/shell boundary used by local tools
|
|
76
|
-
# @param
|
|
77
|
-
#
|
|
104
|
+
# @param workspace_targets [Tools::WorkspaceTargets, nil] additional host-scoped worktree roles
|
|
105
|
+
# @param prompt [Object, nil] interactive prompt bridge; tools such as
|
|
106
|
+
# `ask_user_question` and `open_editor` are advertised only when supported
|
|
78
107
|
# @param web_search [WebSearch] live web search implementation
|
|
79
108
|
# @param web_fetch [WebFetch] specific URL fetch implementation
|
|
80
109
|
# @param code_search [CodeSearch] public source/package search implementation
|
|
81
110
|
# @param web_search_enabled [Boolean, nil] override for web search exposure
|
|
82
111
|
# @param skills [Array<ConfigFiles::Skill>, nil] override discovered skills
|
|
83
112
|
# @param ask_user_question_enabled [Boolean, nil] override question exposure
|
|
84
|
-
|
|
113
|
+
# @param plugin_tools [Array<PluginRegistry::Tool>] trusted plugin tool registrations
|
|
114
|
+
def initialize(workspace: Workspace.new, workspace_targets: nil, prompt: nil, web_search: WebSearch.new, web_fetch: WebFetch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil, allowed_tool_names: nil, editor_prompt_session: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil, plugin_tools: [], typesafe_client: TypeSafeClient.new, tool_approval: nil, approval_for_allowed_tools: false, permission_policy: nil, hook_manager: nil, hook_context: nil, git_committer: nil)
|
|
85
115
|
@workspace = workspace
|
|
116
|
+
@workspace_targets = workspace_targets || Tools::WorkspaceTargets.new(active: workspace)
|
|
86
117
|
@prompt = prompt
|
|
87
118
|
@web_search = web_search
|
|
88
119
|
@web_fetch = web_fetch
|
|
89
120
|
@code_search = code_search
|
|
121
|
+
@typesafe_client = typesafe_client
|
|
90
122
|
@skills = skills
|
|
91
123
|
@web_search_enabled = web_search_enabled
|
|
92
124
|
@ask_user_question_enabled = ask_user_question_enabled
|
|
@@ -96,6 +128,7 @@ module Kward
|
|
|
96
128
|
@tool_output_compactor = tool_output_compactor
|
|
97
129
|
@telemetry_logger = telemetry_logger
|
|
98
130
|
@context_budget_meter = context_budget_meter
|
|
131
|
+
@plugin_tools = Array(plugin_tools)
|
|
99
132
|
@tool_approval = tool_approval
|
|
100
133
|
@approval_for_allowed_tools = approval_for_allowed_tools == true
|
|
101
134
|
@permission_policy = permission_policy || Permissions::Policy.from_config(ConfigFiles.read_config)
|
|
@@ -113,6 +146,16 @@ module Kward
|
|
|
113
146
|
@schemas = build_schema_tools.map { |tool| schema_with_metadata(tool) }.freeze
|
|
114
147
|
end
|
|
115
148
|
|
|
149
|
+
# Replaces plugin registrations while preserving this registry's frontend,
|
|
150
|
+
# workspace, permission, hook, and execution-profile restrictions.
|
|
151
|
+
def replace_plugin_tools!(plugin_tools)
|
|
152
|
+
@plugin_tools = Array(plugin_tools)
|
|
153
|
+
@plugin_tool_values = nil
|
|
154
|
+
@tools = build_tools.freeze
|
|
155
|
+
@schemas = build_schema_tools.map { |tool| schema_with_metadata(tool) }.freeze
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
116
159
|
# Builds a registry scoped to one editor prompt turn.
|
|
117
160
|
#
|
|
118
161
|
# Editor prompts may inspect the workspace, but their only write operation
|
|
@@ -162,7 +205,10 @@ module Kward
|
|
|
162
205
|
original_content = if tool
|
|
163
206
|
before_tool = run_hook("tool_call_before", conversation, payload: tool_payload(name, args, tool_call))
|
|
164
207
|
args = hook_arguments(before_tool, args)
|
|
165
|
-
|
|
208
|
+
target_error = workspace_target_error(name, args)
|
|
209
|
+
if target_error
|
|
210
|
+
target_error
|
|
211
|
+
elsif before_tool.denied?
|
|
166
212
|
hook_denied_content(before_tool, "tool call denied: #{name}")
|
|
167
213
|
elsif before_tool.approval_required? && hook_approval_denied?(before_tool, tool_call, name, args, cancellation)
|
|
168
214
|
hook_denied_content(before_tool, "tool call approval denied: #{name}")
|
|
@@ -253,11 +299,13 @@ module Kward
|
|
|
253
299
|
case tool
|
|
254
300
|
when Tools::MCPTool
|
|
255
301
|
"mcp"
|
|
302
|
+
when Tools::PluginTool
|
|
303
|
+
"plugin"
|
|
256
304
|
when Tools::WebSearch, Tools::FetchContent, Tools::FetchRaw
|
|
257
305
|
"web"
|
|
258
306
|
when Tools::ReadSkill
|
|
259
307
|
"skill"
|
|
260
|
-
when Tools::AskUserQuestion
|
|
308
|
+
when Tools::AskUserQuestion, Tools::OpenEditor
|
|
261
309
|
"ui"
|
|
262
310
|
else
|
|
263
311
|
"builtin"
|
|
@@ -490,6 +538,13 @@ module Kward
|
|
|
490
538
|
(metadata[:source] || metadata["source"]).to_s == "mcp"
|
|
491
539
|
end
|
|
492
540
|
|
|
541
|
+
def workspace_target_error(name, args)
|
|
542
|
+
return nil unless TARGETED_WORKSPACE_TOOLS.include?(name.to_s)
|
|
543
|
+
return nil if @workspace_targets.valid?(args)
|
|
544
|
+
|
|
545
|
+
"Error: Unknown workspace target: #{@workspace_targets.target_name(args)}"
|
|
546
|
+
end
|
|
547
|
+
|
|
493
548
|
def permission_approval_result(tool_call, name, args, cancellation)
|
|
494
549
|
return false unless @tool_approval
|
|
495
550
|
|
|
@@ -513,6 +568,11 @@ module Kward
|
|
|
513
568
|
tools = all_tools
|
|
514
569
|
tools = tools.select { |tool| @allowed_tool_names.include?(tool.name) } if @allowed_tool_names
|
|
515
570
|
tools.each_with_object({}) do |tool, result|
|
|
571
|
+
if tool.is_a?(Tools::PluginTool) && (BUILTIN_TOOL_NAMES.include?(tool.name) || result.key?(tool.name))
|
|
572
|
+
source = tool.plugin_path.to_s.empty? ? "" : " (#{tool.plugin_path})"
|
|
573
|
+
ConfigFiles.emit_warning("Warning: skipping Kward plugin tool #{tool.name}#{source}: duplicate tool name")
|
|
574
|
+
next
|
|
575
|
+
end
|
|
516
576
|
raise ArgumentError, "Duplicate tool name: #{tool.name}" if result.key?(tool.name)
|
|
517
577
|
|
|
518
578
|
result[tool.name] = tool
|
|
@@ -522,10 +582,13 @@ module Kward
|
|
|
522
582
|
def build_schema_tools
|
|
523
583
|
tools = @tools.values_at(*CORE_TOOL_NAMES)
|
|
524
584
|
tools << @tools["replace_editor_buffer"] if @tools["replace_editor_buffer"]
|
|
585
|
+
tools << @tools["open_editor"] if @tools["open_editor"]
|
|
525
586
|
tools << @tools["prepare_shell_command"] if @tools["prepare_shell_command"]
|
|
526
587
|
tools << @tools["git_commit"] if @tools["git_commit"]
|
|
527
588
|
tools.concat(@tools.values_at("web_search", "fetch_content", "fetch_raw")) if web_search_available?
|
|
528
589
|
tools.concat(@tools.values.select { |tool| tool.is_a?(Tools::MCPTool) })
|
|
590
|
+
tools.concat(@tools.values.select { |tool| tool.is_a?(Tools::PluginTool) })
|
|
591
|
+
tools << @tools["typesafe_evaluate"] if @tools["typesafe_evaluate"]
|
|
529
592
|
tools << @tools["read_skill"] if skills_available?
|
|
530
593
|
tools << @tools["ask_user_question"] if ask_user_question_available?
|
|
531
594
|
tools.compact
|
|
@@ -533,30 +596,32 @@ module Kward
|
|
|
533
596
|
|
|
534
597
|
def all_tools
|
|
535
598
|
tools = core_tools
|
|
536
|
-
tools << Tools::GitCommit.new(committer: @git_committer) if @git_committer
|
|
537
|
-
tools + [
|
|
599
|
+
tools << Tools::GitCommit.new(committer: @git_committer, workspace_targets: @workspace_targets) if @git_committer
|
|
600
|
+
(tools + [
|
|
601
|
+
(Tools::TypeSafeEvaluate.new(client: @typesafe_client) if @typesafe_client&.available?),
|
|
538
602
|
Tools::WebSearch.new(web_search: @web_search),
|
|
539
603
|
Tools::FetchContent.new(web_fetch: @web_fetch),
|
|
540
604
|
Tools::FetchRaw.new(web_fetch: @web_fetch),
|
|
541
605
|
Tools::ReadSkill.new(skills: discovered_skills),
|
|
542
606
|
Tools::AskUserQuestion.new(prompt: @prompt)
|
|
543
|
-
] + mcp_tool_values
|
|
607
|
+
]).compact + mcp_tool_values + plugin_tool_values
|
|
544
608
|
end
|
|
545
609
|
|
|
546
610
|
def core_tools
|
|
547
611
|
tools = [
|
|
548
|
-
Tools::ListDirectory.new(workspace: @workspace),
|
|
549
|
-
Tools::ReadFile.new(workspace: @workspace),
|
|
550
|
-
Tools::WriteFile.new(workspace: @workspace),
|
|
551
|
-
Tools::EditFile.new(workspace: @workspace),
|
|
612
|
+
Tools::ListDirectory.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
613
|
+
Tools::ReadFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
614
|
+
Tools::WriteFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
615
|
+
Tools::EditFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
552
616
|
shell_command_tool,
|
|
553
617
|
Tools::CodeSearch.new(code_search: @code_search),
|
|
554
|
-
Tools::SummarizeFileStructure.new(workspace: @workspace),
|
|
555
|
-
Tools::ContextForTask.new(workspace: @workspace),
|
|
618
|
+
Tools::SummarizeFileStructure.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
619
|
+
Tools::ContextForTask.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
556
620
|
Tools::ContextBudgetStats.new(context_budget_meter: @context_budget_meter),
|
|
557
621
|
Tools::RetrieveToolOutput.new
|
|
558
622
|
]
|
|
559
623
|
tools << Tools::ReplaceEditorBuffer.new(editor_prompt_session: @editor_prompt_session) if @editor_prompt_session
|
|
624
|
+
tools << Tools::OpenEditor.new(workspace: @workspace, prompt: @prompt) if editor_available?
|
|
560
625
|
tools << Tools::PrepareShellCommand.new(shell_prompt_session: @shell_prompt_session) if @shell_prompt_session
|
|
561
626
|
tools
|
|
562
627
|
end
|
|
@@ -565,10 +630,14 @@ module Kward
|
|
|
565
630
|
if @shell_prompt_session
|
|
566
631
|
Tools::RunShellCommand.new(shell_prompt_session: @shell_prompt_session)
|
|
567
632
|
else
|
|
568
|
-
Tools::RunShellCommand.new(workspace: @workspace)
|
|
633
|
+
Tools::RunShellCommand.new(workspace: @workspace, workspace_targets: @workspace_targets)
|
|
569
634
|
end
|
|
570
635
|
end
|
|
571
636
|
|
|
637
|
+
def editor_available?
|
|
638
|
+
@prompt&.respond_to?(:edit_file)
|
|
639
|
+
end
|
|
640
|
+
|
|
572
641
|
def editor_prompt_tool_names
|
|
573
642
|
readonly_tools = CORE_TOOL_NAMES - %w[write_file edit_file run_shell_command]
|
|
574
643
|
readonly_tools + ["replace_editor_buffer"]
|
|
@@ -576,7 +645,9 @@ module Kward
|
|
|
576
645
|
|
|
577
646
|
def shell_prompt_tool_names
|
|
578
647
|
readonly_tools = CORE_TOOL_NAMES - %w[write_file edit_file run_shell_command]
|
|
579
|
-
readonly_tools + %w[run_shell_command prepare_shell_command]
|
|
648
|
+
tools = readonly_tools + %w[run_shell_command prepare_shell_command]
|
|
649
|
+
tools << "open_editor" if editor_available?
|
|
650
|
+
tools
|
|
580
651
|
end
|
|
581
652
|
|
|
582
653
|
def web_search_available?
|
|
@@ -602,6 +673,27 @@ module Kward
|
|
|
602
673
|
@mcp_tool_values ||= build_mcp_tools
|
|
603
674
|
end
|
|
604
675
|
|
|
676
|
+
def plugin_tool_values
|
|
677
|
+
@plugin_tool_values ||= @plugin_tools.map do |registration|
|
|
678
|
+
Tools::PluginTool.new(
|
|
679
|
+
registration: registration,
|
|
680
|
+
context_factory: method(:plugin_tool_context)
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def plugin_tool_context(conversation, cancellation)
|
|
686
|
+
if @hook_context.respond_to?(:for_tool)
|
|
687
|
+
@hook_context.for_tool(conversation: conversation, cancellation: cancellation)
|
|
688
|
+
else
|
|
689
|
+
PluginRegistry::Context.new(
|
|
690
|
+
conversation: conversation,
|
|
691
|
+
workspace_root: @workspace.respond_to?(:root) ? @workspace.root.to_s : Dir.pwd,
|
|
692
|
+
cancellation: cancellation
|
|
693
|
+
)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
605
697
|
def build_mcp_tools
|
|
606
698
|
Array(@mcp_clients).flat_map do |client|
|
|
607
699
|
client.list_tools.map do |tool|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative "base"
|
|
2
|
-
require_relative "../workspace"
|
|
2
|
+
require_relative "../workspace/workspace"
|
|
3
3
|
|
|
4
4
|
# Namespace for the Kward CLI agent runtime.
|
|
5
5
|
module Kward
|
|
@@ -8,11 +8,11 @@ module Kward
|
|
|
8
8
|
# Tool wrapper for bounded shell commands in the workspace or active shell.
|
|
9
9
|
class RunShellCommand < Base
|
|
10
10
|
# Builds the tool schema and stores the execution dependency.
|
|
11
|
-
def initialize(workspace: nil, shell_prompt_session: nil)
|
|
11
|
+
def initialize(workspace: nil, workspace_targets: nil, shell_prompt_session: nil)
|
|
12
12
|
raise ArgumentError, "RunShellCommand requires a workspace or shell session" unless workspace || shell_prompt_session
|
|
13
13
|
raise ArgumentError, "RunShellCommand accepts only one execution target" if workspace && shell_prompt_session
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
configure_workspace_targets(workspace, workspace_targets) if workspace
|
|
16
16
|
@shell_prompt_session = shell_prompt_session
|
|
17
17
|
description = shell_prompt_session ?
|
|
18
18
|
"Run a bounded noninteractive command in the active embedded shell session." :
|
|
@@ -20,13 +20,15 @@ module Kward
|
|
|
20
20
|
timeout_description = shell_prompt_session ?
|
|
21
21
|
"Timeout seconds; defaults to the embedded shell setting." :
|
|
22
22
|
"Timeout seconds; default 30."
|
|
23
|
+
properties = {
|
|
24
|
+
command: { type: "string", description: "Command to run." },
|
|
25
|
+
timeout_seconds: { type: "integer", description: timeout_description }
|
|
26
|
+
}
|
|
27
|
+
properties = targeted_properties(properties) if workspace
|
|
23
28
|
super(
|
|
24
29
|
"run_shell_command",
|
|
25
30
|
description,
|
|
26
|
-
properties:
|
|
27
|
-
command: { type: "string", description: "Command to run." },
|
|
28
|
-
timeout_seconds: { type: "integer", description: timeout_description }
|
|
29
|
-
},
|
|
31
|
+
properties: properties,
|
|
30
32
|
required: ["command"]
|
|
31
33
|
)
|
|
32
34
|
end
|
|
@@ -43,7 +45,7 @@ module Kward
|
|
|
43
45
|
if @shell_prompt_session
|
|
44
46
|
@shell_prompt_session.run(command, timeout_seconds: timeout_seconds, cancellation: cancellation)
|
|
45
47
|
else
|
|
46
|
-
|
|
48
|
+
workspace_for(args).run_shell_command(command, timeout_seconds: timeout_seconds, cancellation: cancellation)
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -7,14 +7,14 @@ module Kward
|
|
|
7
7
|
# Returns a compact symbol outline for a workspace source file.
|
|
8
8
|
class SummarizeFileStructure < Base
|
|
9
9
|
# Builds the tool schema and stores the execution dependency.
|
|
10
|
-
def initialize(workspace:)
|
|
11
|
-
|
|
10
|
+
def initialize(workspace:, workspace_targets: nil)
|
|
11
|
+
configure_workspace_targets(workspace, workspace_targets)
|
|
12
12
|
super(
|
|
13
13
|
"summarize_file_structure",
|
|
14
14
|
"Return a compact outline of classes, modules, methods, and functions in a workspace source file.",
|
|
15
|
-
properties:
|
|
15
|
+
properties: targeted_properties(
|
|
16
16
|
path: { type: "string", description: "Workspace-relative source file path." }
|
|
17
|
-
|
|
17
|
+
),
|
|
18
18
|
required: ["path"]
|
|
19
19
|
)
|
|
20
20
|
end
|
|
@@ -22,7 +22,7 @@ module Kward
|
|
|
22
22
|
# Executes the structure summary tool.
|
|
23
23
|
def call(args, _conversation, cancellation: nil)
|
|
24
24
|
cancellation&.raise_if_cancelled!
|
|
25
|
-
|
|
25
|
+
workspace_for(args).summarize_file_structure(argument(args, :path, ""))
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -26,7 +26,9 @@ module Kward
|
|
|
26
26
|
"fetch_raw" => "fetch_raw",
|
|
27
27
|
"git_commit" => "git_commit",
|
|
28
28
|
"read_skill" => "read_skill",
|
|
29
|
-
"ask_user_question" => "ask_user_question"
|
|
29
|
+
"ask_user_question" => "ask_user_question",
|
|
30
|
+
"open_editor" => "open_editor",
|
|
31
|
+
"typesafe_evaluate" => "typesafe_evaluate"
|
|
30
32
|
}.freeze
|
|
31
33
|
|
|
32
34
|
module_function
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "base"
|
|
3
|
+
require_relative "../model/typesafe_client"
|
|
4
|
+
|
|
5
|
+
module Kward
|
|
6
|
+
module Tools
|
|
7
|
+
# Evaluates bounded state with TypeSafe's typed decision model.
|
|
8
|
+
class TypeSafeEvaluate < Base
|
|
9
|
+
def initialize(client: TypeSafeClient.new)
|
|
10
|
+
@client = client
|
|
11
|
+
super(
|
|
12
|
+
"typesafe_evaluate",
|
|
13
|
+
"Evaluate text with TypeSafe and return typed decisions, probabilities, and confidence. Use for classification, scoring, routing, or verification; this does not generate text.",
|
|
14
|
+
properties: {
|
|
15
|
+
state: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The text or structured state to evaluate; do not include secrets unless explicitly requested."
|
|
18
|
+
},
|
|
19
|
+
model: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "TypeSafe model ID or alias; defaults to jev-latest."
|
|
22
|
+
},
|
|
23
|
+
questions: {
|
|
24
|
+
type: "object",
|
|
25
|
+
description: "Named typed questions. Each value must be a noul, choice, or score question.",
|
|
26
|
+
additionalProperties: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
type: { type: "string", enum: %w[noul choice score] },
|
|
30
|
+
instructions: { type: "string" },
|
|
31
|
+
criteria: { type: ["object", "array"] }
|
|
32
|
+
},
|
|
33
|
+
required: %w[type instructions],
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: %w[state questions]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call(args, _conversation, cancellation: nil)
|
|
43
|
+
state = argument(args, "state").to_s
|
|
44
|
+
questions = argument(args, "questions")
|
|
45
|
+
error = validate(state, questions)
|
|
46
|
+
return "Error: #{error}" if error
|
|
47
|
+
|
|
48
|
+
response = @client.evaluate(
|
|
49
|
+
state: state,
|
|
50
|
+
questions: questions,
|
|
51
|
+
model: argument(args, "model") || TypeSafeClient::DEFAULT_MODEL,
|
|
52
|
+
cancellation: cancellation
|
|
53
|
+
)
|
|
54
|
+
JSON.pretty_generate(response)
|
|
55
|
+
rescue TypeSafeClient::RequestError => e
|
|
56
|
+
"Error: #{e.message}"
|
|
57
|
+
rescue JSON::GeneratorError, TypeError => e
|
|
58
|
+
"Error: invalid TypeSafe response: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate(state, questions)
|
|
64
|
+
return "state must be a non-empty string" if state.empty?
|
|
65
|
+
return "state exceeds #{TypeSafeClient::MAX_STATE_BYTES} bytes" if state.bytesize > TypeSafeClient::MAX_STATE_BYTES
|
|
66
|
+
return "questions must be an object" unless questions.is_a?(Hash)
|
|
67
|
+
return "questions must contain 1-#{TypeSafeClient::MAX_QUESTIONS} entries" if questions.empty? || questions.length > TypeSafeClient::MAX_QUESTIONS
|
|
68
|
+
|
|
69
|
+
questions.each do |name, question|
|
|
70
|
+
return "question names must be non-empty strings" unless name.is_a?(String) && !name.empty?
|
|
71
|
+
return "question #{name.inspect} must be an object" unless question.is_a?(Hash)
|
|
72
|
+
type = argument(question, "type").to_s
|
|
73
|
+
return "question #{name.inspect} has unsupported type" unless %w[noul choice score].include?(type)
|
|
74
|
+
return "question #{name.inspect} needs instructions" if argument(question, "instructions").to_s.empty?
|
|
75
|
+
return "question #{name.inspect} needs criteria" if %w[choice score].include?(type) && !argument(question, "criteria")
|
|
76
|
+
end
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Model-callable tool wrappers and their argument schemas.
|
|
4
|
+
module Tools
|
|
5
|
+
# Resolves a small host-scoped set of workspace roles for local tools.
|
|
6
|
+
# Target names are supplied by trusted runtime orchestration, never by the
|
|
7
|
+
# model, so selecting a target cannot escape into an arbitrary filesystem
|
|
8
|
+
# path.
|
|
9
|
+
class WorkspaceTargets
|
|
10
|
+
ACTIVE = "active"
|
|
11
|
+
|
|
12
|
+
def initialize(active:, targets: {})
|
|
13
|
+
@workspaces = { ACTIVE => active }
|
|
14
|
+
targets.each do |name, workspace|
|
|
15
|
+
name = name.to_s
|
|
16
|
+
raise ArgumentError, "Workspace target name is required" if name.empty?
|
|
17
|
+
raise ArgumentError, "Workspace target #{ACTIVE.inspect} is reserved" if name == ACTIVE
|
|
18
|
+
|
|
19
|
+
@workspaces[name] = workspace
|
|
20
|
+
end
|
|
21
|
+
@workspaces.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def workspace_for(args)
|
|
25
|
+
@workspaces.fetch(target_name(args)) do |name|
|
|
26
|
+
raise ArgumentError, "Unknown workspace target: #{name}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def target_name(args)
|
|
31
|
+
value = if args.respond_to?(:key?)
|
|
32
|
+
args[:target] || args["target"]
|
|
33
|
+
end
|
|
34
|
+
value.to_s.empty? ? ACTIVE : value.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def valid?(args)
|
|
38
|
+
@workspaces.key?(target_name(args))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def properties(properties)
|
|
42
|
+
return properties if @workspaces.length == 1
|
|
43
|
+
|
|
44
|
+
properties.merge(
|
|
45
|
+
target: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: @workspaces.keys,
|
|
48
|
+
description: "Workspace role. Defaults to active; origin is the host repository worktree."
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def without_target(args)
|
|
54
|
+
args.reject { |key, _value| key.to_s == "target" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|