kward 0.67.1 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +48 -0
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +54 -0
  5. data/Gemfile.lock +8 -2
  6. data/README.md +37 -30
  7. data/Rakefile +14 -1
  8. data/doc/authentication.md +84 -43
  9. data/doc/code-search.md +55 -28
  10. data/doc/configuration.md +27 -2
  11. data/doc/extensibility.md +90 -129
  12. data/doc/getting-started.md +53 -57
  13. data/doc/memory.md +51 -118
  14. data/doc/personas.md +417 -0
  15. data/doc/plugins.md +55 -99
  16. data/doc/releasing.md +10 -9
  17. data/doc/rpc.md +7 -7
  18. data/doc/usage.md +125 -141
  19. data/doc/web-search.md +80 -14
  20. data/exe/kward +2 -0
  21. data/kward.gemspec +4 -0
  22. data/lib/kward/agent.rb +30 -3
  23. data/lib/kward/ansi.rb +3 -0
  24. data/lib/kward/auth/anthropic_oauth.rb +291 -0
  25. data/lib/kward/auth/file.rb +2 -0
  26. data/lib/kward/auth/github_oauth.rb +3 -0
  27. data/lib/kward/auth/openai_oauth.rb +4 -0
  28. data/lib/kward/auth/openrouter_api_key.rb +2 -0
  29. data/lib/kward/cancellation.rb +3 -0
  30. data/lib/kward/cli/auth_commands.rb +82 -0
  31. data/lib/kward/cli/commands.rb +229 -0
  32. data/lib/kward/cli/compaction.rb +25 -0
  33. data/lib/kward/cli/doctor.rb +121 -0
  34. data/lib/kward/cli/interactive_turn.rb +227 -0
  35. data/lib/kward/cli/memory_commands.rb +133 -0
  36. data/lib/kward/cli/plugins.rb +112 -0
  37. data/lib/kward/cli/prompt_interface.rb +134 -0
  38. data/lib/kward/cli/rendering.rb +378 -0
  39. data/lib/kward/cli/runtime_helpers.rb +170 -0
  40. data/lib/kward/cli/sessions.rb +376 -0
  41. data/lib/kward/cli/settings.rb +669 -0
  42. data/lib/kward/cli/slash_commands.rb +114 -0
  43. data/lib/kward/cli/stats.rb +64 -0
  44. data/lib/kward/cli/sysprompt.rb +57 -0
  45. data/lib/kward/cli/tool_summaries.rb +157 -0
  46. data/lib/kward/cli.rb +52 -2792
  47. data/lib/kward/cli_transcript_formatter.rb +40 -12
  48. data/lib/kward/clipboard.rb +1 -0
  49. data/lib/kward/compaction/file_operation_tracker.rb +3 -0
  50. data/lib/kward/compactor.rb +31 -9
  51. data/lib/kward/config_files.rb +78 -34
  52. data/lib/kward/conversation.rb +110 -13
  53. data/lib/kward/events.rb +2 -0
  54. data/lib/kward/export_path.rb +2 -0
  55. data/lib/kward/image_attachments.rb +2 -0
  56. data/lib/kward/markdown_transcript.rb +2 -0
  57. data/lib/kward/memory/manager.rb +144 -14
  58. data/lib/kward/message_access.rb +29 -2
  59. data/lib/kward/message_text.rb +45 -0
  60. data/lib/kward/model/chat_invocation.rb +2 -0
  61. data/lib/kward/model/client.rb +295 -77
  62. data/lib/kward/model/context_overflow.rb +2 -0
  63. data/lib/kward/model/context_usage.rb +14 -10
  64. data/lib/kward/model/model_info.rb +160 -4
  65. data/lib/kward/model/payloads.rb +254 -22
  66. data/lib/kward/model/retry_message.rb +2 -0
  67. data/lib/kward/model/stream_parser.rb +387 -25
  68. data/lib/kward/pan/server.rb +3 -1
  69. data/lib/kward/plugin_registry.rb +12 -0
  70. data/lib/kward/private_file.rb +2 -0
  71. data/lib/kward/prompt_interface/banner.rb +3 -0
  72. data/lib/kward/prompt_interface/composer_controller.rb +262 -0
  73. data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
  74. data/lib/kward/prompt_interface/composer_state.rb +221 -0
  75. data/lib/kward/prompt_interface/key_handler.rb +365 -0
  76. data/lib/kward/prompt_interface/layout.rb +31 -0
  77. data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
  78. data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
  79. data/lib/kward/prompt_interface/question_prompt.rb +328 -0
  80. data/lib/kward/prompt_interface/runtime_state.rb +59 -0
  81. data/lib/kward/prompt_interface/screen.rb +186 -0
  82. data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
  83. data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
  84. data/lib/kward/prompt_interface/stream_state.rb +65 -0
  85. data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
  86. data/lib/kward/prompt_interface/transcript_renderer.rb +151 -0
  87. data/lib/kward/prompt_interface.rb +69 -1832
  88. data/lib/kward/prompts/commands.rb +2 -0
  89. data/lib/kward/prompts/templates.rb +3 -0
  90. data/lib/kward/prompts.rb +63 -7
  91. data/lib/kward/question_contract.rb +66 -0
  92. data/lib/kward/resources/avatar_kward_logo.rb +2 -0
  93. data/lib/kward/resources/pixel_logo.rb +2 -0
  94. data/lib/kward/rpc/attachment_normalizer.rb +60 -0
  95. data/lib/kward/rpc/auth_manager.rb +65 -11
  96. data/lib/kward/rpc/config_manager.rb +11 -0
  97. data/lib/kward/rpc/prompt_bridge.rb +5 -26
  98. data/lib/kward/rpc/redactor.rb +3 -0
  99. data/lib/kward/rpc/runtime_payloads.rb +4 -1
  100. data/lib/kward/rpc/server.rb +43 -11
  101. data/lib/kward/rpc/session_manager.rb +139 -347
  102. data/lib/kward/rpc/session_metrics.rb +68 -0
  103. data/lib/kward/rpc/session_tree.rb +48 -0
  104. data/lib/kward/rpc/session_tree_rows.rb +208 -0
  105. data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
  106. data/lib/kward/rpc/tool_metadata.rb +3 -0
  107. data/lib/kward/rpc/transcript_normalizer.rb +50 -0
  108. data/lib/kward/rpc/transport.rb +3 -0
  109. data/lib/kward/session_diff.rb +2 -0
  110. data/lib/kward/session_store.rb +154 -25
  111. data/lib/kward/session_trash.rb +1 -0
  112. data/lib/kward/session_tree_renderer.rb +8 -41
  113. data/lib/kward/session_tree_tool_display.rb +56 -0
  114. data/lib/kward/skills/registry.rb +3 -0
  115. data/lib/kward/starter_pack_installer.rb +3 -2
  116. data/lib/kward/steering.rb +2 -0
  117. data/lib/kward/telemetry/logger.rb +3 -0
  118. data/lib/kward/telemetry/stats.rb +3 -0
  119. data/lib/kward/tools/ask_user_question.rb +20 -32
  120. data/lib/kward/tools/base.rb +8 -0
  121. data/lib/kward/tools/code_search.rb +5 -0
  122. data/lib/kward/tools/edit_file.rb +5 -0
  123. data/lib/kward/tools/fetch_content.rb +41 -0
  124. data/lib/kward/tools/fetch_raw.rb +40 -0
  125. data/lib/kward/tools/list_directory.rb +5 -0
  126. data/lib/kward/tools/read_file.rb +5 -0
  127. data/lib/kward/tools/read_skill.rb +5 -0
  128. data/lib/kward/tools/registry.rb +42 -4
  129. data/lib/kward/tools/run_shell_command.rb +5 -0
  130. data/lib/kward/tools/search/code.rb +7 -0
  131. data/lib/kward/tools/search/web.rb +20 -17
  132. data/lib/kward/tools/search/web_fetch.rb +202 -0
  133. data/lib/kward/tools/tool_call.rb +27 -5
  134. data/lib/kward/tools/web_search.rb +7 -1
  135. data/lib/kward/tools/write_file.rb +5 -0
  136. data/lib/kward/transcript_export.rb +2 -0
  137. data/lib/kward/version.rb +2 -1
  138. data/lib/kward/workspace.rb +45 -5
  139. data/templates/default/fulldoc/html/css/kward.css +1501 -0
  140. data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
  141. data/templates/default/fulldoc/html/js/kward.js +296 -0
  142. data/templates/default/fulldoc/html/setup.rb +8 -0
  143. data/templates/default/layout/html/breadcrumb.erb +11 -0
  144. data/templates/default/layout/html/layout.erb +141 -0
  145. data/templates/default/layout/html/setup.rb +139 -0
  146. metadata +56 -1
@@ -0,0 +1,114 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Interactive slash-command parsing and dispatch helpers.
6
+ module SlashCommands
7
+ private
8
+
9
+ def handle_local_slash_command(command, agent, session_store)
10
+ name, argument = parse_slash_command(command)
11
+ case name
12
+ when "status"
13
+ run_busy_local_command_and_requeue { print_status }
14
+ [true, nil]
15
+ when "stats"
16
+ run_busy_local_command_and_requeue { print_stats(argument) }
17
+ [true, nil]
18
+ when "memory"
19
+ activity = memory_summarize_command?(argument) ? "summarizing" : "loading"
20
+ run_busy_local_command_and_requeue(activity: activity) { handle_memory_command(argument, agent) }
21
+ [true, nil]
22
+ when "redraw"
23
+ run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
24
+ [true, nil]
25
+ when "settings"
26
+ configure_settings(agent.conversation)
27
+ [true, nil]
28
+ when "login"
29
+ login_interactively
30
+ [true, nil]
31
+ when "model"
32
+ models = run_busy_local_command_and_requeue { normalized_available_models }
33
+ configure_model(agent.conversation, models: models)
34
+ [true, nil]
35
+ when "openrouter/catalog"
36
+ run_busy_local_command_and_requeue { print_openrouter_catalog }
37
+ [true, nil]
38
+ when "reasoning"
39
+ configure_reasoning(agent.conversation)
40
+ [true, nil]
41
+ when "reload"
42
+ run_busy_local_command_and_requeue { reload_plugins(agent.conversation) }
43
+ [true, nil]
44
+ when "new"
45
+ [true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
46
+ when "resume"
47
+ [true, run_busy_local_command_and_requeue do
48
+ path = argument.to_s.strip
49
+ path = select_session_path(session_store) if session_store && path.empty?
50
+ resume_session(session_store, path)
51
+ end]
52
+ when "name"
53
+ run_busy_local_command_and_requeue { rename_session(argument) }
54
+ [true, nil]
55
+ when "clone"
56
+ [true, run_busy_local_command_and_requeue { clone_session(session_store, agent) }]
57
+ when "tree"
58
+ [true, run_busy_local_command_and_requeue { navigate_session_tree(session_store) }]
59
+ when "copy"
60
+ run_busy_local_command_and_requeue { copy_session_text(agent.conversation, argument) }
61
+ [true, nil]
62
+ when "export"
63
+ run_busy_local_command_and_requeue { export_session(agent.conversation, argument) }
64
+ [true, nil]
65
+ when "compact"
66
+ run_busy_local_command_and_requeue(activity: "compacting") { compact_context(agent, argument) }
67
+ [true, nil]
68
+ else
69
+ if plugin_command_for(name)
70
+ run_busy_local_command_and_requeue(activity: "running") { run_plugin_command(name, argument, agent) }
71
+ else
72
+ [false, nil]
73
+ end
74
+ end
75
+ end
76
+
77
+ def parse_slash_command(command)
78
+ PromptCommands.parse(command) || [nil, ""]
79
+ end
80
+
81
+ # Writes the status output for the terminal CLI flow.
82
+ def print_status
83
+ lines = [STATUS_MESSAGE]
84
+ lines << ""
85
+ lines << auto_compaction_status_line
86
+ if @active_session
87
+ lines << "Session: #{@active_session.name || @active_session.id}"
88
+ lines << "File: #{@active_session.path}"
89
+ end
90
+ lines.compact!
91
+ runtime_output(lines.join("\n"))
92
+ end
93
+
94
+ def auto_compaction_status_line
95
+ settings = Kward::Compaction::Settings.from_config
96
+ return "Auto-compaction: disabled" unless settings.enabled
97
+
98
+ context_window = composer_context_window
99
+ return "Auto-compaction: enabled, unknown context window" unless context_window.to_i.positive?
100
+
101
+ reserve_tokens = Kward::Compactor.auto_compaction_reserve_tokens(
102
+ context_window: context_window,
103
+ configured_reserve_tokens: settings.reserve_tokens
104
+ )
105
+ percent = ((reserve_tokens.to_f / context_window.to_i) * 100).round(1)
106
+ "Auto-compaction reserve: #{reserve_tokens} tokens (#{percent}% of #{context_window})"
107
+ rescue StandardError => e
108
+ warn "Auto-compaction status unavailable: #{e.message}"
109
+ nil
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,64 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Token statistics command helpers mixed into the CLI frontend.
6
+ module Stats
7
+ private
8
+
9
+ def export_token_stats(arguments)
10
+ options = parse_token_stats_options(arguments)
11
+ csv = TelemetryStats.new.token_usage_csv(options[:range], bucket: options[:bucket])
12
+ if options[:output]
13
+ File.write(options[:output], csv)
14
+ else
15
+ $stdout.write(csv)
16
+ end
17
+ rescue ArgumentError => e
18
+ warn e.message
19
+ warn "Usage: kward stats tokens [range] [--bucket second|minute|hour|day|week|month|year] [--output path]"
20
+ exit 1
21
+ end
22
+
23
+ def parse_token_stats_options(arguments)
24
+ remaining = []
25
+ bucket = nil
26
+ output = nil
27
+ index = 0
28
+ while index < arguments.length
29
+ argument = arguments[index]
30
+ case argument
31
+ when "--bucket"
32
+ index += 1
33
+ raise ArgumentError, "Missing value for --bucket" if index >= arguments.length
34
+
35
+ bucket = arguments[index]
36
+ when /\A--bucket=(.+)\z/
37
+ bucket = Regexp.last_match(1)
38
+ when "--output"
39
+ index += 1
40
+ raise ArgumentError, "Missing value for --output" if index >= arguments.length
41
+
42
+ output = arguments[index]
43
+ when /\A--output=(.+)\z/
44
+ output = Regexp.last_match(1)
45
+ else
46
+ remaining << argument
47
+ end
48
+ index += 1
49
+ end
50
+ { range: remaining.join(" "), bucket: bucket, output: output }
51
+ end
52
+
53
+ # Writes the stats output for the terminal CLI flow.
54
+ def print_stats(argument)
55
+ result = TelemetryStats.new.collect(argument)
56
+ runtime_output(TelemetryStats.format(result))
57
+ rescue ArgumentError => e
58
+ message = e.message == TelemetryStats::USAGE ? e.message : "#{e.message}\n#{TelemetryStats::USAGE}"
59
+ runtime_output(message)
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # System prompt inspection command helpers.
6
+ module Sysprompt
7
+ private
8
+
9
+ def print_sysprompt(arguments)
10
+ raw = parse_sysprompt_arguments(arguments)
11
+ conversation = new_conversation
12
+ content = conversation.system_message.fetch(:content)
13
+ if raw
14
+ @prompt.say(content)
15
+ else
16
+ @prompt.say(render_markdown_transcript(render_sysprompt_sections(conversation)))
17
+ end
18
+ end
19
+
20
+ def parse_sysprompt_arguments(arguments)
21
+ raw = false
22
+ arguments.each do |argument|
23
+ case argument
24
+ when "--raw"
25
+ raw = true
26
+ else
27
+ raise ArgumentError, command_usage("sysprompt")
28
+ end
29
+ end
30
+ raw
31
+ end
32
+
33
+ def render_sysprompt_sections(conversation)
34
+ sections = Prompts.prompt_sections(
35
+ workspace_root: conversation.workspace_root,
36
+ model: conversation.model,
37
+ reasoning_effort: conversation.reasoning_effort,
38
+ memory_context: conversation.memory_context,
39
+ plugin_context: conversation.last_plugin_prompt_context
40
+ )
41
+ lines = ["# Kward System Prompt", "", "Workspace: #{conversation.workspace_root}"]
42
+ lines << "Model: #{[conversation.provider, conversation.model].compact.join(" / ")}" unless conversation.model.to_s.empty?
43
+ lines << "Reasoning effort: #{conversation.reasoning_effort}" unless conversation.reasoning_effort.to_s.empty?
44
+ lines << "Memory: not included; memory context is retrieved per user turn."
45
+ sections.each do |section|
46
+ lines << ""
47
+ lines << "## #{section.fetch(:label)}"
48
+ source = section[:source]
49
+ lines << "Source: #{source}" unless source.to_s.empty?
50
+ lines << ""
51
+ lines << section.fetch(:content)
52
+ end
53
+ lines.join("\n")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,157 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
4
+ class CLI
5
+ # Compact tool-output summaries for terminal display and restored transcripts.
6
+ module ToolSummaries
7
+ private
8
+
9
+ def tool_result_summary(tool_call, content)
10
+ name = tool_call_name(tool_call)
11
+ args = tool_call_args(tool_call)
12
+ text = content.to_s
13
+ return error_tool_summary(name, args, text) if tool_result_failed?(text)
14
+
15
+ case name
16
+ when "read_file"
17
+ read_file_summary(args, text)
18
+ when "write_file", "edit_file"
19
+ file_change_summary(name, args, text)
20
+ when "run_shell_command"
21
+ shell_command_summary(args, text)
22
+ when "web_search"
23
+ web_search_summary(args, text)
24
+ else
25
+ generic_tool_summary(name, text)
26
+ end
27
+ end
28
+
29
+ def tool_result_failed?(content)
30
+ content.to_s.start_with?("Error:", "Declined:", "Cancelled.")
31
+ end
32
+
33
+ def limit_tool_output_lines(content, line_limit)
34
+ lines = content.to_s.lines
35
+ return content.to_s if lines.length <= line_limit
36
+
37
+ kept_lines = lines.first(line_limit - 1).join
38
+ omitted_lines = lines.length - (line_limit - 1)
39
+ suffix = omitted_lines == 1 ? "line" : "lines"
40
+ notice = "...[truncated #{omitted_lines} #{suffix}]"
41
+ kept_lines.end_with?("\n") || kept_lines.empty? ? "#{kept_lines}#{notice}" : "#{kept_lines}\n#{notice}"
42
+ end
43
+
44
+ def read_file_summary(args, content)
45
+ path = args["path"] || args[:path] || "(unknown path)"
46
+ "read_file: #{path}\n#{content.lines.count} lines, #{content.bytesize} bytes"
47
+ end
48
+
49
+ def file_change_summary(name, args, content)
50
+ path = args["path"] || args[:path] || path_from_tool_result(content) || "(unknown path)"
51
+ concise = content.lines.first.to_s.strip
52
+ concise = "completed" if concise.empty?
53
+ "#{name}: #{path}\n#{concise}"
54
+ end
55
+
56
+ def shell_command_summary(args, content)
57
+ command = args["command"] || args[:command] || ""
58
+ lines = ["run_shell_command: #{command}".strip]
59
+ lines << "Exit status: #{shell_exit_status(content) || "unknown"}"
60
+ stdout = shell_section(content, "STDOUT")
61
+ stderr = shell_section(content, "STDERR")
62
+ lines << compact_stream_summary("stdout", stdout) unless stdout.empty?
63
+ lines << compact_stream_summary("stderr", stderr) unless stderr.empty?
64
+ lines.join("\n")
65
+ end
66
+
67
+ def web_search_summary(args, content)
68
+ queries = Array(args["queries"] || args[:queries]).map(&:to_s)
69
+ queries = web_search_queries_from_content(content) if queries.empty?
70
+ counts = web_search_result_counts(content)
71
+ lines = ["web_search"]
72
+ queries.each do |query|
73
+ lines << "#{query}: #{counts.fetch(query, 0)} result(s)"
74
+ end
75
+ lines << "#{web_search_total_count(content)} result(s)" if queries.empty?
76
+ lines.join("\n")
77
+ end
78
+
79
+ def error_tool_summary(name, args, content)
80
+ path = args["path"] || args[:path]
81
+ command = args["command"] || args[:command]
82
+ context = path || command
83
+ [name, context, content.lines.first.to_s.strip].compact.reject(&:empty?).join("\n")
84
+ end
85
+
86
+ def generic_tool_summary(name, content)
87
+ text = content.to_s
88
+ return "#{name}: #{text}" if text.length <= RESTORED_TOOL_OUTPUT_LIMIT
89
+
90
+ "#{name}: #{text[0, RESTORED_TOOL_OUTPUT_LIMIT]}\n...[truncated #{text.length - RESTORED_TOOL_OUTPUT_LIMIT} bytes]"
91
+ end
92
+
93
+ def compact_stream_summary(label, text)
94
+ summary = text.strip
95
+ summary = summary[0, 500] + "\n...[truncated #{summary.length - 500} chars]" if summary.length > 500
96
+ "#{label} (#{text.bytesize} bytes):#{summary.empty? ? "" : "\n#{summary}"}"
97
+ end
98
+
99
+ def shell_exit_status(content)
100
+ content.match(/^Exit status: ([^\n]+)/)&.[](1)
101
+ end
102
+
103
+ def shell_section(content, name)
104
+ match = content.match(/^#{Regexp.escape(name)}:\n(.*?)(?=\nSTD(?:OUT|ERR):\n|\z)/m)
105
+ match ? match[1] : ""
106
+ end
107
+
108
+ def web_search_queries_from_content(content)
109
+ content.scan(/^## Query: (.+)$/).flatten
110
+ end
111
+
112
+ def web_search_result_counts(content)
113
+ counts = {}
114
+ current_query = nil
115
+ content.each_line do |line|
116
+ if (match = line.match(/^## Query: (.+)$/))
117
+ current_query = match[1]
118
+ counts[current_query] ||= 0
119
+ elsif current_query && line.match?(/^\d+\. /)
120
+ counts[current_query] += 1
121
+ end
122
+ end
123
+ counts
124
+ end
125
+
126
+ def web_search_total_count(content)
127
+ content.each_line.count { |line| line.match?(/^\d+\. /) }
128
+ end
129
+
130
+ def path_from_tool_result(content)
131
+ content.match(/\b(?:to|file|Edited)\s+([^:\n]+?)(?:\s|:|\z)/)&.[](1)
132
+ end
133
+
134
+ def tool_call_name(tool_call)
135
+ ToolCall.name(tool_call) || "unknown_tool"
136
+ end
137
+
138
+ def tool_call_args(tool_call)
139
+ ToolCall.arguments(tool_call)
140
+ end
141
+
142
+ def tool_command(tool_call)
143
+ name = tool_call_name(tool_call)
144
+ args = tool_call_args(tool_call)
145
+
146
+ if name == "run_shell_command"
147
+ args["command"] || args[:command] || ""
148
+ elsif args.empty?
149
+ name.to_s
150
+ else
151
+ "#{name} #{JSON.dump(args)}"
152
+ end
153
+ end
154
+
155
+ end
156
+ end
157
+ end