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
|
@@ -1,659 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "hooks"
|
|
4
|
-
require_relative "transport"
|
|
5
|
-
|
|
6
|
-
# Namespace for the Kward CLI agent runtime.
|
|
7
|
-
module Kward
|
|
8
|
-
# Loads trusted user plugin files and provides the plugin DSL.
|
|
9
|
-
#
|
|
10
|
-
# Plugins live in the user plugin directory, run as local Ruby code, and can
|
|
11
|
-
# register slash commands, one footer renderer, prompt context, and live
|
|
12
|
-
# transcript-event observers for CLI and RPC frontends.
|
|
13
|
-
#
|
|
14
|
-
# This registry is intentionally trust-based, not a sandbox. Keep plugin loading
|
|
15
|
-
# restricted to `ConfigFiles.plugin_paths`, keep workspace-local code out of the
|
|
16
|
-
# load path, and expose immutable transcript views so plugins can observe state
|
|
17
|
-
# without corrupting active conversations.
|
|
18
|
-
class PluginRegistry
|
|
19
|
-
COMMAND_NAME_PATTERN = /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/.freeze
|
|
20
|
-
|
|
21
|
-
# Registered slash command exposed in completion, RPC command listings, and
|
|
22
|
-
# interactive command dispatch.
|
|
23
|
-
Command = Struct.new(:name, :description, :argument_hint, :path, :handler, keyword_init: true) do
|
|
24
|
-
def entry
|
|
25
|
-
{ name: name, description: description, argument_hint: argument_hint }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Registered interactive command that takes over the composer region with a
|
|
30
|
-
# Kward-driven render and input loop. Like a slash command but with canvas
|
|
31
|
-
# rendering capabilities for games, dashboards, viewers, and similar uses.
|
|
32
|
-
InteractiveCommand = Struct.new(:name, :description, :argument_hint, :rows, :fps, :path, :handler, keyword_init: true) do
|
|
33
|
-
def entry
|
|
34
|
-
{ name: name, description: description, argument_hint: argument_hint }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Registered plugin-owned tab runtime. Its factory receives a
|
|
39
|
-
# `PluginTabHost` and its persisted descriptor, then returns a driver.
|
|
40
|
-
TabType = Struct.new(:id, :name, :title, :singleton, :rpc, :transport, :local, :transcript_events, :path, :handler, keyword_init: true)
|
|
41
|
-
|
|
42
|
-
# Registered external transport. The factory receives a transport host and
|
|
43
|
-
# configuration when the transport is started, not while plugins load.
|
|
44
|
-
TransportType = Struct.new(:id, :name, :capabilities, :execution_profile, :path, :handler, keyword_init: true)
|
|
45
|
-
|
|
46
|
-
# Read-only event passed to plugin transcript observers.
|
|
47
|
-
TranscriptEvent = Struct.new(:type, :payload, keyword_init: true) do
|
|
48
|
-
def to_h
|
|
49
|
-
{ type: type, payload: payload }
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Registered lifecycle hook handler.
|
|
54
|
-
HookHandler = Struct.new(:event, :id, :description, :path, :order, :match, :failure_policy, :handler, keyword_init: true)
|
|
55
|
-
|
|
56
|
-
# Read-only transcript view exposed to plugin code.
|
|
57
|
-
class Transcript
|
|
58
|
-
# Creates an object for trusted plugin loading and dispatch.
|
|
59
|
-
def initialize(conversation)
|
|
60
|
-
@conversation = conversation
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Returns a deep-frozen copy of the active conversation messages.
|
|
64
|
-
#
|
|
65
|
-
# @return [Array<Hash>] immutable transcript message data
|
|
66
|
-
def messages
|
|
67
|
-
DeepCopy.freeze(DeepCopy.dup(@conversation.messages))
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Runtime context passed to plugin commands, footers, prompt context
|
|
72
|
-
# renderers, and transcript event handlers.
|
|
73
|
-
class Context
|
|
74
|
-
attr_reader :args, :workspace_root
|
|
75
|
-
|
|
76
|
-
# Creates an object for trusted plugin loading and dispatch.
|
|
77
|
-
def initialize(conversation:, args: "", session: nil, workspace_root: Dir.pwd, say_callback: nil)
|
|
78
|
-
@conversation = conversation
|
|
79
|
-
@args = args.to_s
|
|
80
|
-
@session = session
|
|
81
|
-
@workspace_root = workspace_root
|
|
82
|
-
@say_callback = say_callback
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# @return [Transcript] read-only transcript wrapper
|
|
86
|
-
def transcript
|
|
87
|
-
Transcript.new(@conversation)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Emits command output to the active frontend when available.
|
|
91
|
-
#
|
|
92
|
-
# @param message [#to_s] message to display
|
|
93
|
-
# @return [nil]
|
|
94
|
-
def say(message)
|
|
95
|
-
@say_callback&.call(message.to_s)
|
|
96
|
-
nil
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# @return [String, nil] active session identifier
|
|
100
|
-
def session_id
|
|
101
|
-
@session&.id
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# @return [String, nil] human-readable active session name
|
|
105
|
-
def session_name
|
|
106
|
-
@session&.name
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# @return [String, nil] saved active session path
|
|
110
|
-
def session_path
|
|
111
|
-
@session&.path
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Requests that the conversation rebuild its system message after plugin
|
|
115
|
-
# state changes that affect prompt context.
|
|
116
|
-
#
|
|
117
|
-
# @return [nil]
|
|
118
|
-
def refresh_system_message!
|
|
119
|
-
@conversation.refresh_system_message! if @conversation.respond_to?(:refresh_system_message!)
|
|
120
|
-
nil
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Allows the current lifecycle event to continue.
|
|
124
|
-
# @return [Hooks::Decision]
|
|
125
|
-
def allow(message = nil, metadata: nil)
|
|
126
|
-
Hooks::Decision.allow(message, metadata: metadata)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Denies the current lifecycle event.
|
|
130
|
-
# @return [Hooks::Decision]
|
|
131
|
-
def deny(message = nil, metadata: nil)
|
|
132
|
-
Hooks::Decision.deny(message, metadata: metadata)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Requests frontend approval for the current lifecycle event.
|
|
136
|
-
# @return [Hooks::Decision]
|
|
137
|
-
def ask(message = nil, metadata: nil)
|
|
138
|
-
Hooks::Decision.ask(message, metadata: metadata)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Continues with an event-specific payload replacement.
|
|
142
|
-
# @param payload [Hash] replacement fields supported by the event
|
|
143
|
-
# @return [Hooks::Decision]
|
|
144
|
-
def modify(payload, message: nil, metadata: nil)
|
|
145
|
-
Hooks::Decision.modify(payload, message: message, metadata: metadata)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Allows the event while recording a warning.
|
|
149
|
-
# @return [Hooks::Decision]
|
|
150
|
-
def warn(message = nil, metadata: nil)
|
|
151
|
-
Hooks::Decision.warn(message, metadata: metadata)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Requests a retry when the current event supports it.
|
|
155
|
-
# @return [Hooks::Decision]
|
|
156
|
-
def retry(message = nil, payload: nil, metadata: nil)
|
|
157
|
-
Hooks::Decision.retry(message, payload: payload, metadata: metadata)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Defers the event when the current workflow supports it.
|
|
161
|
-
# @return [Hooks::Decision]
|
|
162
|
-
def defer(message = nil, payload: nil, metadata: nil)
|
|
163
|
-
Hooks::Decision.defer(message, payload: payload, metadata: metadata)
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Public DSL object yielded by `Kward.plugin` blocks.
|
|
168
|
-
#
|
|
169
|
-
# Plugin files normally interact with this object only through a block:
|
|
170
|
-
#
|
|
171
|
-
# @example Register a plugin command
|
|
172
|
-
# Kward.plugin do |plugin|
|
|
173
|
-
# plugin.command "hello", description: "Say hello" do |args, ctx|
|
|
174
|
-
# name = args.strip.empty? ? "there" : args.strip
|
|
175
|
-
# ctx.say "Hello, #{name}."
|
|
176
|
-
# end
|
|
177
|
-
# end
|
|
178
|
-
#
|
|
179
|
-
# @api public
|
|
180
|
-
class DSL
|
|
181
|
-
# Creates an object for trusted plugin loading and dispatch.
|
|
182
|
-
def initialize(registry, path)
|
|
183
|
-
@registry = registry
|
|
184
|
-
@path = path
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Registers a slash command.
|
|
188
|
-
#
|
|
189
|
-
# The command is available in the interactive CLI and through the RPC
|
|
190
|
-
# command bridge. Command names do not include the leading `/`.
|
|
191
|
-
#
|
|
192
|
-
# @param name [String, #to_s] command name without the leading slash
|
|
193
|
-
# @param description [String] short text shown in command listings
|
|
194
|
-
# @param argument_hint [String] optional usage hint for arguments
|
|
195
|
-
# @yieldparam args [String] text after the command name
|
|
196
|
-
# @yieldparam ctx [Context] plugin execution context
|
|
197
|
-
# @return [void]
|
|
198
|
-
# @api public
|
|
199
|
-
def command(name, description: "", argument_hint: "", &block)
|
|
200
|
-
@registry.register_command(name, description: description, argument_hint: argument_hint, path: @path, &block)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Registers or replaces the custom footer renderer.
|
|
204
|
-
#
|
|
205
|
-
# Only one footer renderer is active. If multiple plugins register one,
|
|
206
|
-
# the later renderer replaces the earlier renderer.
|
|
207
|
-
#
|
|
208
|
-
# @yieldparam ctx [Context] plugin execution context
|
|
209
|
-
# @return [void]
|
|
210
|
-
# @api public
|
|
211
|
-
def footer(&block)
|
|
212
|
-
@registry.register_footer(path: @path, &block)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Registers a live transcript event observer.
|
|
216
|
-
#
|
|
217
|
-
# Observer errors are caught and reported as warnings so a plugin cannot
|
|
218
|
-
# crash the active turn by raising from an event handler.
|
|
219
|
-
#
|
|
220
|
-
# @yieldparam event [TranscriptEvent] normalized transcript event
|
|
221
|
-
# @yieldparam ctx [Context] plugin execution context
|
|
222
|
-
# @return [void]
|
|
223
|
-
# @api public
|
|
224
|
-
def on_transcript_event(&block)
|
|
225
|
-
@registry.register_transcript_event(path: @path, &block)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Registers a lifecycle hook handler.
|
|
229
|
-
#
|
|
230
|
-
# Hooks are deterministic runtime callbacks around Kward lifecycle events.
|
|
231
|
-
# They can return a {Hooks::Decision}, a decision hash, a decision string,
|
|
232
|
-
# or nil to allow the operation.
|
|
233
|
-
#
|
|
234
|
-
# @param event [String, #to_s] lifecycle event name
|
|
235
|
-
# @param id [String, nil] stable hook identifier for logs and diagnostics
|
|
236
|
-
# @param description [String] short human-readable purpose
|
|
237
|
-
# @param order [Integer] lower values run first
|
|
238
|
-
# @param match [Hash, nil] optional event selector
|
|
239
|
-
# @yieldparam event [Hooks::Event] immutable lifecycle event
|
|
240
|
-
# @yieldparam ctx [Context] plugin execution context and decision helpers
|
|
241
|
-
# @return [void]
|
|
242
|
-
# @api public
|
|
243
|
-
def hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, &block)
|
|
244
|
-
@registry.register_hook(event, id: id, description: description, order: order, match: match, failure_policy: failure_policy, path: @path, &block)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Registers prompt context text injected into future system prompts.
|
|
248
|
-
#
|
|
249
|
-
# Keep this text short and never include secrets. The returned string can
|
|
250
|
-
# be sent to the active model as part of Kward's system instructions.
|
|
251
|
-
#
|
|
252
|
-
# @yieldparam ctx [Context] plugin execution context
|
|
253
|
-
# @return [void]
|
|
254
|
-
# @api public
|
|
255
|
-
def prompt_context(&block)
|
|
256
|
-
@registry.register_prompt_context(path: @path, &block)
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Registers an interactive command that takes over the composer region with
|
|
260
|
-
# a Kward-driven render and input loop. The handler receives an
|
|
261
|
-
# interactive controller object with a canvas API for drawing colored
|
|
262
|
-
# cells and reading keys. Useful for games, dashboards, and viewers.
|
|
263
|
-
#
|
|
264
|
-
# @param name [String, #to_s] command name without the leading slash
|
|
265
|
-
# @param rows [Integer] fixed canvas height in terminal rows
|
|
266
|
-
# @param fps [Numeric] frame rate for tick callbacks (1-120, default 30)
|
|
267
|
-
# @param description [String] short text shown in command listings
|
|
268
|
-
# @param argument_hint [String] optional usage hint for arguments
|
|
269
|
-
# @yieldparam ui [Object] interactive controller with canvas and key API
|
|
270
|
-
# @yieldparam ctx [Context] plugin execution context
|
|
271
|
-
# @return [void]
|
|
272
|
-
# @api public
|
|
273
|
-
def interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", &block)
|
|
274
|
-
@registry.register_interactive_command(name, rows: rows, fps: fps, description: description, argument_hint: argument_hint, path: @path, &block)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Registers a plugin-owned chat type. `id` is a durable identifier used
|
|
278
|
-
# in persisted tab layouts and transport chat handles and must not change.
|
|
279
|
-
# The factory receives a `PluginTabHost` and a descriptor hash.
|
|
280
|
-
#
|
|
281
|
-
# @param name [String] command name used by `/tab open <name>`
|
|
282
|
-
# @param id [String] stable persisted tab type identifier
|
|
283
|
-
# @param title [String] default tab label
|
|
284
|
-
# @param singleton [Symbol] `:global` for one shared plugin runtime
|
|
285
|
-
# @param rpc [Boolean] expose this chat through trusted local RPC
|
|
286
|
-
# @param transport [Boolean] allow external transport adapters to target this chat
|
|
287
|
-
# @param local [Boolean] expose this chat as an interactive local tab
|
|
288
|
-
# @param transcript_events [Boolean] allow global transcript observers to receive this tab's events
|
|
289
|
-
# @yieldparam host [PluginTabHost] supported host dependencies
|
|
290
|
-
# @yieldparam descriptor [Hash] persisted tab descriptor
|
|
291
|
-
# @return [void]
|
|
292
|
-
# @api public
|
|
293
|
-
def tab_type(name, id:, title: nil, singleton: nil, rpc: false, transport: false, local: true, transcript_events: false, &block)
|
|
294
|
-
@registry.register_tab_type(name, id: id, title: title, singleton: singleton, rpc: rpc, transport: transport, local: local, transcript_events: transcript_events, path: @path, &block)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Registers an external messaging or event transport. The factory is
|
|
298
|
-
# called when the transport runtime starts.
|
|
299
|
-
#
|
|
300
|
-
# @param name [String] human-readable transport name
|
|
301
|
-
# @param id [String] stable transport identifier
|
|
302
|
-
# @param capabilities [Hash, Transport::Capabilities] supported features
|
|
303
|
-
# @yieldparam host [Object] transport host
|
|
304
|
-
# @yieldparam config [Object] transport configuration
|
|
305
|
-
# @return [void]
|
|
306
|
-
# @api public
|
|
307
|
-
def transport(name, id:, capabilities: nil, execution_profile: nil, &block)
|
|
308
|
-
@registry.register_transport(name, id: id, capabilities: capabilities, execution_profile: execution_profile, path: @path, &block)
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# Mutable singleton guard used while loading trusted plugin files.
|
|
313
|
-
class << self
|
|
314
|
-
attr_accessor :loading_registry, :loading_path
|
|
315
|
-
|
|
316
|
-
def load(paths: nil, reserved_commands: [], warning_sink: nil)
|
|
317
|
-
warning_sink ||= ConfigFiles.warning_sink
|
|
318
|
-
paths ||= ConfigFiles.plugin_paths(warning_sink: warning_sink)
|
|
319
|
-
registry = new(reserved_commands: reserved_commands, warning_sink: warning_sink)
|
|
320
|
-
paths.each { |path| registry.load_file(path) }
|
|
321
|
-
registry
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
# Creates an object for trusted plugin loading and dispatch.
|
|
326
|
-
def initialize(reserved_commands: [], warning_sink: nil)
|
|
327
|
-
@reserved_commands = reserved_commands.map(&:to_s)
|
|
328
|
-
@warning_sink = warning_sink
|
|
329
|
-
@commands = {}
|
|
330
|
-
@interactive_commands = {}
|
|
331
|
-
@tab_types = {}
|
|
332
|
-
@tab_types_by_id = {}
|
|
333
|
-
@transports = {}
|
|
334
|
-
@transports_by_id = {}
|
|
335
|
-
@footer = nil
|
|
336
|
-
@footer_path = nil
|
|
337
|
-
@transcript_event_handlers = []
|
|
338
|
-
@prompt_context_renderers = []
|
|
339
|
-
@hook_handlers = []
|
|
340
|
-
@paths = []
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# @return [String, nil] plugin file currently responsible for footer output
|
|
344
|
-
attr_reader :footer_path
|
|
345
|
-
|
|
346
|
-
# @return [Array<String>] plugin files successfully loaded by this registry
|
|
347
|
-
attr_reader :paths
|
|
348
|
-
|
|
349
|
-
def commands
|
|
350
|
-
@commands.values
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def command_for(name)
|
|
354
|
-
@commands[name.to_s]
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def interactive_commands
|
|
358
|
-
@interactive_commands.values
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def interactive_command_for(name)
|
|
362
|
-
@interactive_commands[name.to_s]
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def tab_types
|
|
366
|
-
@tab_types.values
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def tab_type_for(name)
|
|
370
|
-
@tab_types[name.to_s]
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def tab_type_for_id(id)
|
|
374
|
-
@tab_types_by_id[id.to_s]
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def transport_tab_types
|
|
378
|
-
@tab_types.values.select(&:transport)
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def transports
|
|
382
|
-
@transports.values
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def transport_for(name)
|
|
386
|
-
@transports[name.to_s]
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def transport_for_id(id)
|
|
390
|
-
@transports_by_id[id.to_s]
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def footer_renderer
|
|
394
|
-
@footer
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
def transcript_event_handlers
|
|
398
|
-
@transcript_event_handlers.map { |entry| entry[:handler] }
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def prompt_context_renderers
|
|
402
|
-
@prompt_context_renderers.map { |entry| entry[:renderer] }
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def hook_handlers
|
|
406
|
-
@hook_handlers.dup
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def hook_manager
|
|
410
|
-
manager = Hooks::Manager.new
|
|
411
|
-
@hook_handlers.each do |hook|
|
|
412
|
-
manager.register(hook.event, id: hook.id, source: hook.path, order: hook.order, match: hook.match, failure_policy: hook.failure_policy) do |event, context|
|
|
413
|
-
hook.handler.call(event, context)
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
manager
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def prompt_context(context)
|
|
420
|
-
parts = []
|
|
421
|
-
@prompt_context_renderers.each do |entry|
|
|
422
|
-
rendered = entry[:renderer].call(context)
|
|
423
|
-
parts << rendered.to_s unless rendered.to_s.empty?
|
|
424
|
-
rescue StandardError => e
|
|
425
|
-
emit_warning "Warning: Kward plugin prompt context error in #{entry[:path]}: #{e.message}"
|
|
426
|
-
end
|
|
427
|
-
parts.empty? ? nil : parts.join("\n\n")
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
def notify_transcript_event(event, context)
|
|
431
|
-
transcript_event = transcript_event_for(event)
|
|
432
|
-
return unless transcript_event
|
|
433
|
-
|
|
434
|
-
@transcript_event_handlers.each do |entry|
|
|
435
|
-
entry[:handler].call(transcript_event, context)
|
|
436
|
-
rescue StandardError => e
|
|
437
|
-
emit_warning "Warning: Kward plugin transcript event error in #{entry[:path]}: #{e.message}"
|
|
438
|
-
end
|
|
439
|
-
nil
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
def load_file(path)
|
|
443
|
-
previous_registry = self.class.loading_registry
|
|
444
|
-
previous_path = self.class.loading_path
|
|
445
|
-
self.class.loading_registry = self
|
|
446
|
-
self.class.loading_path = path
|
|
447
|
-
Kernel.load(path, true)
|
|
448
|
-
@paths << path
|
|
449
|
-
rescue StandardError => e
|
|
450
|
-
emit_warning "Warning: skipping Kward plugin #{path}: #{e.message}"
|
|
451
|
-
ensure
|
|
452
|
-
self.class.loading_registry = previous_registry
|
|
453
|
-
self.class.loading_path = previous_path
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def evaluate(path: nil, &block)
|
|
457
|
-
dsl = DSL.new(self, path)
|
|
458
|
-
block.arity == 1 ? block.call(dsl) : dsl.instance_eval(&block)
|
|
459
|
-
self
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
def register_command(name, description: "", argument_hint: "", path: nil, &handler)
|
|
463
|
-
name = name.to_s
|
|
464
|
-
raise "Plugin command name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
|
|
465
|
-
raise "Plugin command /#{name} requires a handler" unless handler
|
|
466
|
-
|
|
467
|
-
if @reserved_commands.include?(name)
|
|
468
|
-
emit_warning "Warning: skipping Kward plugin command /#{name}: reserved command"
|
|
469
|
-
return nil
|
|
470
|
-
end
|
|
471
|
-
if @commands.key?(name)
|
|
472
|
-
emit_warning "Warning: skipping duplicate Kward plugin command /#{name}: #{path}"
|
|
473
|
-
return nil
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
@commands[name] = Command.new(
|
|
477
|
-
name: name,
|
|
478
|
-
description: description.to_s,
|
|
479
|
-
argument_hint: argument_hint.to_s,
|
|
480
|
-
path: path,
|
|
481
|
-
handler: handler
|
|
482
|
-
)
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def register_interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", path: nil, &handler)
|
|
486
|
-
name = name.to_s
|
|
487
|
-
raise "Interactive command name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
|
|
488
|
-
raise "Interactive command /#{name} requires a handler" unless handler
|
|
489
|
-
|
|
490
|
-
if @reserved_commands.include?(name) || @commands.key?(name)
|
|
491
|
-
emit_warning "Warning: skipping Kward interactive command /#{name}: reserved command"
|
|
492
|
-
return nil
|
|
493
|
-
end
|
|
494
|
-
if @interactive_commands.key?(name)
|
|
495
|
-
emit_warning "Warning: skipping duplicate Kward interactive command /#{name}: #{path}"
|
|
496
|
-
return nil
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
@interactive_commands[name] = InteractiveCommand.new(
|
|
500
|
-
name: name,
|
|
501
|
-
description: description.to_s,
|
|
502
|
-
argument_hint: argument_hint.to_s,
|
|
503
|
-
rows: [[rows.to_i, 1].max, 1].max,
|
|
504
|
-
fps: [[fps.to_f, 1].max, 120].min,
|
|
505
|
-
path: path,
|
|
506
|
-
handler: handler
|
|
507
|
-
)
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def register_tab_type(name, id:, title: nil, singleton: nil, rpc: false, transport: false, local: true, transcript_events: false, path: nil, &handler)
|
|
511
|
-
name = name.to_s
|
|
512
|
-
id = id.to_s
|
|
513
|
-
raise "Plugin tab type name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
|
|
514
|
-
raise "Plugin tab type id is required" if id.empty?
|
|
515
|
-
raise "Plugin tab type #{name} requires a handler" unless handler
|
|
516
|
-
|
|
517
|
-
if @tab_types.key?(name) || @tab_types_by_id.key?(id)
|
|
518
|
-
emit_warning "Warning: skipping duplicate Kward plugin tab type #{id}: #{path}"
|
|
519
|
-
return nil
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
tab_type = TabType.new(id: id, name: name, title: title.to_s.empty? ? name.capitalize : title.to_s, singleton: singleton&.to_sym, rpc: rpc == true, transport: transport == true, local: local == true, transcript_events: transcript_events == true, path: path, handler: handler)
|
|
523
|
-
@tab_types[name] = tab_type
|
|
524
|
-
@tab_types_by_id[id] = tab_type
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def register_transport(name, id:, capabilities: nil, execution_profile: nil, path: nil, &handler)
|
|
528
|
-
name = name.to_s
|
|
529
|
-
id = id.to_s
|
|
530
|
-
raise "Plugin transport name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
|
|
531
|
-
raise "Plugin transport id is required" if id.empty?
|
|
532
|
-
raise "Plugin transport #{name} requires a handler" unless handler
|
|
533
|
-
|
|
534
|
-
if @transports.key?(name) || @transports_by_id.key?(id)
|
|
535
|
-
emit_warning "Warning: skipping duplicate Kward plugin transport #{id}: #{path}"
|
|
536
|
-
return nil
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
capabilities = normalize_transport_capabilities(capabilities)
|
|
540
|
-
execution_profile = normalize_execution_profile(execution_profile)
|
|
541
|
-
transport = TransportType.new(id: id, name: name, capabilities: capabilities, execution_profile: execution_profile, path: path, handler: handler)
|
|
542
|
-
@transports[name] = transport
|
|
543
|
-
@transports_by_id[id] = transport
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
def register_footer(path: nil, &renderer)
|
|
547
|
-
raise "Plugin footer requires a renderer" unless renderer
|
|
548
|
-
|
|
549
|
-
emit_warning "Warning: replacing Kward plugin footer from #{@footer_path}: #{path}" if @footer
|
|
550
|
-
@footer = renderer
|
|
551
|
-
@footer_path = path
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def emit_warning(message)
|
|
555
|
-
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def register_transcript_event(path: nil, &handler)
|
|
559
|
-
raise "Plugin transcript event requires a handler" unless handler
|
|
560
|
-
|
|
561
|
-
@transcript_event_handlers << { path: path, handler: handler }
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
def register_prompt_context(path: nil, &renderer)
|
|
565
|
-
raise "Plugin prompt context requires a renderer" unless renderer
|
|
566
|
-
|
|
567
|
-
@prompt_context_renderers << { path: path, renderer: renderer }
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
def register_hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, path: nil, &handler)
|
|
571
|
-
event = event.to_s
|
|
572
|
-
raise "Plugin hook event is required" if event.empty?
|
|
573
|
-
raise "Plugin hook #{event} requires a handler" unless handler
|
|
574
|
-
|
|
575
|
-
@hook_handlers << HookHandler.new(
|
|
576
|
-
event: event,
|
|
577
|
-
id: id&.to_s || "#{File.basename(path.to_s.empty? ? "plugin" : path)}:#{event}:#{@hook_handlers.length + 1}",
|
|
578
|
-
description: description.to_s,
|
|
579
|
-
path: path,
|
|
580
|
-
order: order.to_i,
|
|
581
|
-
match: match,
|
|
582
|
-
failure_policy: failure_policy,
|
|
583
|
-
handler: handler
|
|
584
|
-
)
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
private
|
|
588
|
-
|
|
589
|
-
def normalize_execution_profile(profile)
|
|
590
|
-
return nil if profile.nil?
|
|
591
|
-
return profile if profile.is_a?(Transport::ExecutionProfile)
|
|
592
|
-
raise ArgumentError, "Plugin transport execution_profile must be a Transport::ExecutionProfile"
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
def normalize_transport_capabilities(capabilities)
|
|
596
|
-
return Transport.capabilities if capabilities.nil?
|
|
597
|
-
return capabilities if capabilities.is_a?(Transport::Capabilities)
|
|
598
|
-
raise ArgumentError, "Plugin transport capabilities must be a hash or Transport::Capabilities" unless capabilities.is_a?(Hash)
|
|
599
|
-
|
|
600
|
-
Transport.capabilities(**capabilities.transform_keys(&:to_sym))
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
def transcript_event_for(event)
|
|
604
|
-
case event.class.name
|
|
605
|
-
when "Kward::Events::ReasoningDelta"
|
|
606
|
-
transcript_event("reasoning_delta", delta: event.delta)
|
|
607
|
-
when "Kward::Events::ReasoningBoundary"
|
|
608
|
-
transcript_event("reasoning_boundary")
|
|
609
|
-
when "Kward::Events::AssistantDelta"
|
|
610
|
-
transcript_event("assistant_delta", delta: event.delta)
|
|
611
|
-
when "Kward::Events::AssistantMessage"
|
|
612
|
-
transcript_event("assistant_message", message: event.message)
|
|
613
|
-
when "Kward::Events::Retry"
|
|
614
|
-
transcript_event(
|
|
615
|
-
"model_retry",
|
|
616
|
-
provider: event.provider,
|
|
617
|
-
model: event.model,
|
|
618
|
-
attempt: event.attempt,
|
|
619
|
-
max_attempts: event.max_attempts,
|
|
620
|
-
delay_seconds: event.delay_seconds,
|
|
621
|
-
error: event.error,
|
|
622
|
-
request_bytes: event.request_bytes
|
|
623
|
-
)
|
|
624
|
-
when "Kward::Events::Steering"
|
|
625
|
-
transcript_event("turn_steered", input: event.input, created_at: event.created_at)
|
|
626
|
-
when "Kward::Events::ToolCall"
|
|
627
|
-
transcript_event("tool_call", tool_call: event.tool_call)
|
|
628
|
-
when "Kward::Events::ToolResult"
|
|
629
|
-
transcript_event("tool_result", tool_call: event.tool_call, content: event.content)
|
|
630
|
-
when "Kward::Events::Answer"
|
|
631
|
-
transcript_event("answer", content: event.content)
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
def transcript_event(type, payload = {})
|
|
636
|
-
TranscriptEvent.new(
|
|
637
|
-
type: type,
|
|
638
|
-
payload: DeepCopy.freeze(DeepCopy.dup(payload))
|
|
639
|
-
).freeze
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
# Registers a trusted local plugin.
|
|
644
|
-
#
|
|
645
|
-
# This method is intended for Ruby files loaded from the user plugin
|
|
646
|
-
# directory. It raises if called outside plugin loading so workspace code
|
|
647
|
-
# cannot silently mutate Kward's runtime by merely being required.
|
|
648
|
-
#
|
|
649
|
-
# @yieldparam plugin [PluginRegistry::DSL] plugin registration DSL
|
|
650
|
-
# @return [Object, nil] the plugin block result
|
|
651
|
-
# @api public
|
|
652
|
-
def self.plugin(&block)
|
|
653
|
-
registry = PluginRegistry.loading_registry
|
|
654
|
-
raise "Kward.plugin can only be called while loading a plugin" unless registry
|
|
655
|
-
|
|
656
|
-
dsl = PluginRegistry::DSL.new(registry, PluginRegistry.loading_path)
|
|
657
|
-
block.arity == 1 ? block.call(dsl) : dsl.instance_eval(&block)
|
|
658
|
-
end
|
|
659
|
-
end
|
|
1
|
+
# Compatibility require path for the public plugin registry.
|
|
2
|
+
require_relative "plugins/registry"
|