Skip to content
Kward Search API index

Class: Kward::PluginRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/plugin_registry.rb

Overview

Loads trusted user plugin files and provides the plugin DSL.

Plugins live in the user plugin directory, run as local Ruby code, and can register slash commands, one footer renderer, prompt context, and live transcript-event observers for CLI and RPC frontends.

This registry is intentionally trust-based, not a sandbox. Keep plugin loading restricted to ConfigFiles.plugin_paths, keep workspace-local code out of the load path, and expose immutable transcript views so plugins can observe state without corrupting active conversations.

Defined Under Namespace

Classes: Command, Context, DSL, HookHandler, InteractiveCommand, TabType, Transcript, TranscriptEvent, TransportType

Constant Summary collapse

COMMAND_NAME_PATTERN =
/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(reserved_commands: [], warning_sink: nil) ⇒ PluginRegistry

Creates an object for trusted plugin loading and dispatch.



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/kward/plugin_registry.rb', line 326

def initialize(reserved_commands: [], warning_sink: nil)
  @reserved_commands = reserved_commands.map(&:to_s)
  @warning_sink = warning_sink
  @commands = {}
  @interactive_commands = {}
  @tab_types = {}
  @tab_types_by_id = {}
  @transports = {}
  @transports_by_id = {}
  @footer = nil
  @footer_path = nil
  @transcript_event_handlers = []
  @prompt_context_renderers = []
  @hook_handlers = []
  @paths = []
end

Class Attribute Details

.loading_pathObject

Returns the value of attribute loading_path.



314
315
316
# File 'lib/kward/plugin_registry.rb', line 314

def loading_path
  @loading_path
end

.loading_registryObject

Returns the value of attribute loading_registry.



314
315
316
# File 'lib/kward/plugin_registry.rb', line 314

def loading_registry
  @loading_registry
end

Instance Attribute Details

Returns plugin file currently responsible for footer output.

Returns:

  • (String, nil)

    plugin file currently responsible for footer output



344
345
346
# File 'lib/kward/plugin_registry.rb', line 344

def footer_path
  @footer_path
end

#pathsArray<String> (readonly)

Returns plugin files successfully loaded by this registry.

Returns:

  • (Array<String>)

    plugin files successfully loaded by this registry



347
348
349
# File 'lib/kward/plugin_registry.rb', line 347

def paths
  @paths
end

Class Method Details

.load(paths: nil, reserved_commands: [], warning_sink: nil) ⇒ Object



316
317
318
319
320
321
322
# File 'lib/kward/plugin_registry.rb', line 316

def load(paths: nil, reserved_commands: [], warning_sink: nil)
  warning_sink ||= ConfigFiles.warning_sink
  paths ||= ConfigFiles.plugin_paths(warning_sink: warning_sink)
  registry = new(reserved_commands: reserved_commands, warning_sink: warning_sink)
  paths.each { |path| registry.load_file(path) }
  registry
end

Instance Method Details

#command_for(name) ⇒ Object



353
354
355
# File 'lib/kward/plugin_registry.rb', line 353

def command_for(name)
  @commands[name.to_s]
end

#commandsObject



349
350
351
# File 'lib/kward/plugin_registry.rb', line 349

def commands
  @commands.values
end

#emit_warning(message) ⇒ Object



554
555
556
# File 'lib/kward/plugin_registry.rb', line 554

def emit_warning(message)
  @warning_sink ? @warning_sink.call(message) : warn(message)
end

#evaluate(path: nil, &block) ⇒ Object



456
457
458
459
460
# File 'lib/kward/plugin_registry.rb', line 456

def evaluate(path: nil, &block)
  dsl = DSL.new(self, path)
  block.arity == 1 ? block.call(dsl) : dsl.instance_eval(&block)
  self
end


393
394
395
# File 'lib/kward/plugin_registry.rb', line 393

def footer_renderer
  @footer
end

#hook_handlersObject



405
406
407
# File 'lib/kward/plugin_registry.rb', line 405

def hook_handlers
  @hook_handlers.dup
end

#hook_managerObject



409
410
411
412
413
414
415
416
417
# File 'lib/kward/plugin_registry.rb', line 409

def hook_manager
  manager = Hooks::Manager.new
  @hook_handlers.each do |hook|
    manager.register(hook.event, id: hook.id, source: hook.path, order: hook.order, match: hook.match, failure_policy: hook.failure_policy) do |event, context|
      hook.handler.call(event, context)
    end
  end
  manager
end

#interactive_command_for(name) ⇒ Object



361
362
363
# File 'lib/kward/plugin_registry.rb', line 361

def interactive_command_for(name)
  @interactive_commands[name.to_s]
end

#interactive_commandsObject



357
358
359
# File 'lib/kward/plugin_registry.rb', line 357

def interactive_commands
  @interactive_commands.values
end

#load_file(path) ⇒ Object



442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/kward/plugin_registry.rb', line 442

def load_file(path)
  previous_registry = self.class.loading_registry
  previous_path = self.class.loading_path
  self.class.loading_registry = self
  self.class.loading_path = path
  Kernel.load(path, true)
  @paths << path
rescue StandardError => e
  emit_warning "Warning: skipping Kward plugin #{path}: #{e.message}"
ensure
  self.class.loading_registry = previous_registry
  self.class.loading_path = previous_path
end

#notify_transcript_event(event, context) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
# File 'lib/kward/plugin_registry.rb', line 430

def notify_transcript_event(event, context)
  transcript_event = transcript_event_for(event)
  return unless transcript_event

  @transcript_event_handlers.each do |entry|
    entry[:handler].call(transcript_event, context)
  rescue StandardError => e
    emit_warning "Warning: Kward plugin transcript event error in #{entry[:path]}: #{e.message}"
  end
  nil
end

#prompt_context(context) ⇒ Object



419
420
421
422
423
424
425
426
427
428
# File 'lib/kward/plugin_registry.rb', line 419

def prompt_context(context)
  parts = []
  @prompt_context_renderers.each do |entry|
    rendered = entry[:renderer].call(context)
    parts << rendered.to_s unless rendered.to_s.empty?
  rescue StandardError => e
    emit_warning "Warning: Kward plugin prompt context error in #{entry[:path]}: #{e.message}"
  end
  parts.empty? ? nil : parts.join("\n\n")
end

#prompt_context_renderersObject



401
402
403
# File 'lib/kward/plugin_registry.rb', line 401

def prompt_context_renderers
  @prompt_context_renderers.map { |entry| entry[:renderer] }
end

#register_command(name, description: "", argument_hint: "", path: nil, &handler) ⇒ Object



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/kward/plugin_registry.rb', line 462

def register_command(name, description: "", argument_hint: "", path: nil, &handler)
  name = name.to_s
  raise "Plugin command name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
  raise "Plugin command /#{name} requires a handler" unless handler

  if @reserved_commands.include?(name)
    emit_warning "Warning: skipping Kward plugin command /#{name}: reserved command"
    return nil
  end
  if @commands.key?(name)
    emit_warning "Warning: skipping duplicate Kward plugin command /#{name}: #{path}"
    return nil
  end

  @commands[name] = Command.new(
    name: name,
    description: description.to_s,
    argument_hint: argument_hint.to_s,
    path: path,
    handler: handler
  )
end


546
547
548
549
550
551
552
# File 'lib/kward/plugin_registry.rb', line 546

def register_footer(path: nil, &renderer)
  raise "Plugin footer requires a renderer" unless renderer

  emit_warning "Warning: replacing Kward plugin footer from #{@footer_path}: #{path}" if @footer
  @footer = renderer
  @footer_path = path
end

#register_hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, path: nil, &handler) ⇒ Object



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/kward/plugin_registry.rb', line 570

def register_hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, path: nil, &handler)
  event = event.to_s
  raise "Plugin hook event is required" if event.empty?
  raise "Plugin hook #{event} requires a handler" unless handler

  @hook_handlers << HookHandler.new(
    event: event,
    id: id&.to_s || "#{File.basename(path.to_s.empty? ? "plugin" : path)}:#{event}:#{@hook_handlers.length + 1}",
    description: description.to_s,
    path: path,
    order: order.to_i,
    match: match,
    failure_policy: failure_policy,
    handler: handler
  )
end

#register_interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", path: nil, &handler) ⇒ Object



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/kward/plugin_registry.rb', line 485

def register_interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", path: nil, &handler)
  name = name.to_s
  raise "Interactive command name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
  raise "Interactive command /#{name} requires a handler" unless handler

  if @reserved_commands.include?(name) || @commands.key?(name)
    emit_warning "Warning: skipping Kward interactive command /#{name}: reserved command"
    return nil
  end
  if @interactive_commands.key?(name)
    emit_warning "Warning: skipping duplicate Kward interactive command /#{name}: #{path}"
    return nil
  end

  @interactive_commands[name] = InteractiveCommand.new(
    name: name,
    description: description.to_s,
    argument_hint: argument_hint.to_s,
    rows: [[rows.to_i, 1].max, 1].max,
    fps: [[fps.to_f, 1].max, 120].min,
    path: path,
    handler: handler
  )
end

#register_prompt_context(path: nil, &renderer) ⇒ Object



564
565
566
567
568
# File 'lib/kward/plugin_registry.rb', line 564

def register_prompt_context(path: nil, &renderer)
  raise "Plugin prompt context requires a renderer" unless renderer

  @prompt_context_renderers << { path: path, renderer: renderer }
end

#register_tab_type(name, id:, title: nil, singleton: nil, rpc: false, transport: false, local: true, transcript_events: false, path: nil, &handler) ⇒ Object



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/kward/plugin_registry.rb', line 510

def register_tab_type(name, id:, title: nil, singleton: nil, rpc: false, transport: false, local: true, transcript_events: false, path: nil, &handler)
  name = name.to_s
  id = id.to_s
  raise "Plugin tab type name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
  raise "Plugin tab type id is required" if id.empty?
  raise "Plugin tab type #{name} requires a handler" unless handler

  if @tab_types.key?(name) || @tab_types_by_id.key?(id)
    emit_warning "Warning: skipping duplicate Kward plugin tab type #{id}: #{path}"
    return nil
  end

  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)
  @tab_types[name] = tab_type
  @tab_types_by_id[id] = tab_type
end

#register_transcript_event(path: nil, &handler) ⇒ Object



558
559
560
561
562
# File 'lib/kward/plugin_registry.rb', line 558

def register_transcript_event(path: nil, &handler)
  raise "Plugin transcript event requires a handler" unless handler

  @transcript_event_handlers << { path: path, handler: handler }
end

#register_transport(name, id:, capabilities: nil, execution_profile: nil, path: nil, &handler) ⇒ Object



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/kward/plugin_registry.rb', line 527

def register_transport(name, id:, capabilities: nil, execution_profile: nil, path: nil, &handler)
  name = name.to_s
  id = id.to_s
  raise "Plugin transport name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
  raise "Plugin transport id is required" if id.empty?
  raise "Plugin transport #{name} requires a handler" unless handler

  if @transports.key?(name) || @transports_by_id.key?(id)
    emit_warning "Warning: skipping duplicate Kward plugin transport #{id}: #{path}"
    return nil
  end

  capabilities = normalize_transport_capabilities(capabilities)
  execution_profile = normalize_execution_profile(execution_profile)
  transport = TransportType.new(id: id, name: name, capabilities: capabilities, execution_profile: execution_profile, path: path, handler: handler)
  @transports[name] = transport
  @transports_by_id[id] = transport
end

#tab_type_for(name) ⇒ Object



369
370
371
# File 'lib/kward/plugin_registry.rb', line 369

def tab_type_for(name)
  @tab_types[name.to_s]
end

#tab_type_for_id(id) ⇒ Object



373
374
375
# File 'lib/kward/plugin_registry.rb', line 373

def tab_type_for_id(id)
  @tab_types_by_id[id.to_s]
end

#tab_typesObject



365
366
367
# File 'lib/kward/plugin_registry.rb', line 365

def tab_types
  @tab_types.values
end

#transcript_event_handlersObject



397
398
399
# File 'lib/kward/plugin_registry.rb', line 397

def transcript_event_handlers
  @transcript_event_handlers.map { |entry| entry[:handler] }
end

#transport_for(name) ⇒ Object



385
386
387
# File 'lib/kward/plugin_registry.rb', line 385

def transport_for(name)
  @transports[name.to_s]
end

#transport_for_id(id) ⇒ Object



389
390
391
# File 'lib/kward/plugin_registry.rb', line 389

def transport_for_id(id)
  @transports_by_id[id.to_s]
end

#transport_tab_typesObject



377
378
379
# File 'lib/kward/plugin_registry.rb', line 377

def transport_tab_types
  @tab_types.values.select(&:transport)
end

#transportsObject



381
382
383
# File 'lib/kward/plugin_registry.rb', line 381

def transports
  @transports.values
end