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.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +48 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile.lock +8 -2
- data/README.md +37 -30
- data/Rakefile +14 -1
- data/doc/authentication.md +84 -43
- data/doc/code-search.md +55 -28
- data/doc/configuration.md +27 -2
- data/doc/extensibility.md +90 -129
- data/doc/getting-started.md +53 -57
- data/doc/memory.md +51 -118
- data/doc/personas.md +417 -0
- data/doc/plugins.md +55 -99
- data/doc/releasing.md +10 -9
- data/doc/rpc.md +7 -7
- data/doc/usage.md +125 -141
- data/doc/web-search.md +80 -14
- data/exe/kward +2 -0
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +30 -3
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +229 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +227 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +134 -0
- data/lib/kward/cli/rendering.rb +378 -0
- data/lib/kward/cli/runtime_helpers.rb +170 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +669 -0
- data/lib/kward/cli/slash_commands.rb +114 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/sysprompt.rb +57 -0
- data/lib/kward/cli/tool_summaries.rb +157 -0
- data/lib/kward/cli.rb +52 -2792
- data/lib/kward/cli_transcript_formatter.rb +40 -12
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +31 -9
- data/lib/kward/config_files.rb +78 -34
- data/lib/kward/conversation.rb +110 -13
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +144 -14
- data/lib/kward/message_access.rb +29 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +14 -10
- data/lib/kward/model/model_info.rb +160 -4
- data/lib/kward/model/payloads.rb +254 -22
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +387 -25
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +151 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +63 -7
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +43 -11
- data/lib/kward/rpc/session_manager.rb +139 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +50 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +154 -25
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +3 -2
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/fetch_content.rb +41 -0
- data/lib/kward/tools/fetch_raw.rb +40 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +42 -4
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +20 -17
- data/lib/kward/tools/search/web_fetch.rb +202 -0
- data/lib/kward/tools/tool_call.rb +27 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- data/templates/default/fulldoc/html/css/kward.css +1501 -0
- data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
- data/templates/default/fulldoc/html/js/kward.js +296 -0
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/layout/html/breadcrumb.erb +11 -0
- data/templates/default/layout/html/layout.erb +141 -0
- data/templates/default/layout/html/setup.rb +139 -0
- metadata +56 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require_relative "image_attachments"
|
|
3
3
|
require_relative "message_access"
|
|
4
|
+
require_relative "message_text"
|
|
4
5
|
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
7
|
module Kward
|
|
8
|
+
# Formats conversation messages for terminal transcript display.
|
|
6
9
|
module CLITranscriptFormatter
|
|
7
10
|
module_function
|
|
8
11
|
|
|
@@ -11,13 +14,20 @@ module Kward
|
|
|
11
14
|
return direct.to_s unless direct.to_s.empty?
|
|
12
15
|
|
|
13
16
|
content = MessageAccess.content(message)
|
|
14
|
-
|
|
17
|
+
if content.is_a?(Array)
|
|
18
|
+
text = content.filter_map do |part|
|
|
19
|
+
type = MessageAccess.value(part, :type)
|
|
20
|
+
next unless ["thinking", "reasoning"].include?(type)
|
|
21
|
+
|
|
22
|
+
MessageAccess.value(part, :thinking) || MessageAccess.value(part, :reasoning) || MessageAccess.value(part, :text)
|
|
23
|
+
end.join("\n")
|
|
24
|
+
return text unless text.empty?
|
|
25
|
+
end
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
next unless ["thinking", "reasoning"].include?(type)
|
|
27
|
+
response_items(message).filter_map do |item|
|
|
28
|
+
next unless MessageAccess.value(item, :type) == "reasoning"
|
|
19
29
|
|
|
20
|
-
MessageAccess.value(
|
|
30
|
+
response_item_text(MessageAccess.value(item, :summary)).empty? ? response_item_text(MessageAccess.value(item, :content)) : response_item_text(MessageAccess.value(item, :summary))
|
|
21
31
|
end.join("\n")
|
|
22
32
|
end
|
|
23
33
|
|
|
@@ -30,6 +40,18 @@ module Kward
|
|
|
30
40
|
end
|
|
31
41
|
end
|
|
32
42
|
|
|
43
|
+
def assistant_content_text(message)
|
|
44
|
+
text = content_text(MessageAccess.content(message))
|
|
45
|
+
return text unless text.empty?
|
|
46
|
+
|
|
47
|
+
response_items(message).filter_map do |item|
|
|
48
|
+
next unless MessageAccess.value(item, :type) == "message"
|
|
49
|
+
next if MessageAccess.value(item, :phase).to_s == "commentary"
|
|
50
|
+
|
|
51
|
+
response_item_text(MessageAccess.value(item, :content))
|
|
52
|
+
end.join
|
|
53
|
+
end
|
|
54
|
+
|
|
33
55
|
def display_text(message)
|
|
34
56
|
display_content = MessageAccess.display_content(message)
|
|
35
57
|
return display_content.to_s unless display_content.nil?
|
|
@@ -79,13 +101,7 @@ module Kward
|
|
|
79
101
|
end
|
|
80
102
|
|
|
81
103
|
def full_text(message)
|
|
82
|
-
|
|
83
|
-
text = if content.is_a?(Array)
|
|
84
|
-
content.filter_map { |part| MessageAccess.value(part, :text) }.join("\n")
|
|
85
|
-
else
|
|
86
|
-
content.to_s
|
|
87
|
-
end
|
|
88
|
-
text.strip
|
|
104
|
+
MessageText.full_text(message)
|
|
89
105
|
end
|
|
90
106
|
|
|
91
107
|
def content_part_text(part)
|
|
@@ -99,6 +115,18 @@ module Kward
|
|
|
99
115
|
end
|
|
100
116
|
end
|
|
101
117
|
|
|
118
|
+
def response_items(message)
|
|
119
|
+
MessageAccess.response_items(message)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def response_item_text(parts)
|
|
123
|
+
Array(parts).filter_map do |part|
|
|
124
|
+
next unless part.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
MessageAccess.value(part, :text) || MessageAccess.value(part, :refusal)
|
|
127
|
+
end.join
|
|
128
|
+
end
|
|
129
|
+
|
|
102
130
|
def image_part_reference(part)
|
|
103
131
|
data = MessageAccess.value(part, :data)
|
|
104
132
|
path = MessageAccess.value(part, :path)
|
data/lib/kward/clipboard.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require_relative "../message_access"
|
|
2
2
|
require_relative "../tools/tool_call"
|
|
3
3
|
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
5
|
module Kward
|
|
6
|
+
# Conversation compaction settings, planning, and summary generation.
|
|
5
7
|
module Compaction
|
|
8
|
+
# Tracks file operations while preparing compaction summaries.
|
|
6
9
|
class FileOperationTracker
|
|
7
10
|
def call(messages, previous_details: {})
|
|
8
11
|
read_files = Array(path_values(previous_details, "read_files", :read_files))
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -2,15 +2,23 @@ require "json"
|
|
|
2
2
|
require_relative "model/chat_invocation"
|
|
3
3
|
require_relative "compaction/file_operation_tracker"
|
|
4
4
|
require_relative "config_files"
|
|
5
|
+
require_relative "message_access"
|
|
5
6
|
require_relative "prompts"
|
|
6
7
|
require_relative "tools/tool_call"
|
|
7
8
|
|
|
9
|
+
# Namespace for the Kward CLI agent runtime.
|
|
8
10
|
module Kward
|
|
11
|
+
# Conversation compaction settings, planning, and summary generation.
|
|
9
12
|
module Compaction
|
|
13
|
+
# Compaction support object used by conversation summarization.
|
|
10
14
|
class Error < StandardError; end
|
|
15
|
+
# Compaction support object used by conversation summarization.
|
|
11
16
|
class NothingToCompact < Error; end
|
|
17
|
+
# Compaction support object used by conversation summarization.
|
|
12
18
|
class AlreadyCompacted < Error; end
|
|
19
|
+
# Compaction support object used by conversation summarization.
|
|
13
20
|
class Cancelled < Error; end
|
|
21
|
+
# Compaction support object used by conversation summarization.
|
|
14
22
|
class SummarizationFailed < Error; end
|
|
15
23
|
|
|
16
24
|
PreparationResult = Struct.new(
|
|
@@ -28,6 +36,7 @@ module Kward
|
|
|
28
36
|
|
|
29
37
|
Cut = Struct.new(:first_kept_index, :messages_to_summarize, :turn_prefix_messages, :split_turn, :preserved_messages, :preserved_start_index, keyword_init: true)
|
|
30
38
|
|
|
39
|
+
# Interactive settings menu actions mixed into the CLI frontend.
|
|
31
40
|
class Settings
|
|
32
41
|
DEFAULT_ENABLED = true
|
|
33
42
|
DEFAULT_RESERVE_TOKENS = 16_384
|
|
@@ -35,6 +44,7 @@ module Kward
|
|
|
35
44
|
|
|
36
45
|
attr_reader :enabled, :reserve_tokens, :keep_recent_tokens, :context_window
|
|
37
46
|
|
|
47
|
+
# Creates an object for conversation compaction.
|
|
38
48
|
def initialize(enabled: DEFAULT_ENABLED, reserve_tokens: DEFAULT_RESERVE_TOKENS, keep_recent_tokens: DEFAULT_KEEP_RECENT_TOKENS, context_window: nil)
|
|
39
49
|
@enabled = enabled != false
|
|
40
50
|
@reserve_tokens = positive_integer(reserve_tokens, DEFAULT_RESERVE_TOKENS)
|
|
@@ -64,6 +74,7 @@ module Kward
|
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
|
|
77
|
+
# Compaction support object used by conversation summarization.
|
|
67
78
|
class TokenEstimator
|
|
68
79
|
def estimate_tokens(text)
|
|
69
80
|
(text.to_s.length / 4.0).ceil
|
|
@@ -169,9 +180,11 @@ module Kward
|
|
|
169
180
|
end
|
|
170
181
|
end
|
|
171
182
|
|
|
183
|
+
# Compaction support object used by conversation summarization.
|
|
172
184
|
class ConversationSerializer
|
|
173
185
|
TOOL_RESULT_LIMIT = 2_000
|
|
174
186
|
|
|
187
|
+
# Creates an object for conversation compaction.
|
|
175
188
|
def initialize(tool_result_summarizer: nil)
|
|
176
189
|
@tool_result_summarizer = tool_result_summarizer
|
|
177
190
|
end
|
|
@@ -288,28 +301,27 @@ module Kward
|
|
|
288
301
|
end
|
|
289
302
|
|
|
290
303
|
def message_role(message)
|
|
291
|
-
|
|
304
|
+
MessageAccess.role(message)
|
|
292
305
|
end
|
|
293
306
|
|
|
294
307
|
def message_content(message)
|
|
295
|
-
|
|
308
|
+
MessageAccess.content(message)
|
|
296
309
|
end
|
|
297
310
|
|
|
298
311
|
def message_summary(message)
|
|
299
|
-
|
|
312
|
+
MessageAccess.summary(message) || message_content(message)
|
|
300
313
|
end
|
|
301
314
|
|
|
302
315
|
def message_name(message)
|
|
303
|
-
message
|
|
316
|
+
MessageAccess.tool_name(message)
|
|
304
317
|
end
|
|
305
318
|
|
|
306
319
|
def message_tool_call_id(message)
|
|
307
|
-
|
|
320
|
+
MessageAccess.tool_call_id(message)
|
|
308
321
|
end
|
|
309
322
|
|
|
310
323
|
def message_tool_calls(message)
|
|
311
|
-
|
|
312
|
-
value.is_a?(Array) ? value : []
|
|
324
|
+
MessageAccess.tool_calls(message)
|
|
313
325
|
end
|
|
314
326
|
|
|
315
327
|
def tool_call_id(tool_call)
|
|
@@ -350,9 +362,11 @@ module Kward
|
|
|
350
362
|
end
|
|
351
363
|
end
|
|
352
364
|
|
|
365
|
+
# Compaction support object used by conversation summarization.
|
|
353
366
|
class CutPointFinder
|
|
354
367
|
VALID_CUT_ROLES = ["user", "assistant", "bash", "custom", "branchSummary"].freeze
|
|
355
368
|
|
|
369
|
+
# Creates an object for conversation compaction.
|
|
356
370
|
def initialize(estimator: TokenEstimator.new)
|
|
357
371
|
@estimator = estimator
|
|
358
372
|
end
|
|
@@ -431,7 +445,9 @@ module Kward
|
|
|
431
445
|
end
|
|
432
446
|
end
|
|
433
447
|
|
|
448
|
+
# Compaction support object used by conversation summarization.
|
|
434
449
|
class Preparation
|
|
450
|
+
# Creates an object for conversation compaction.
|
|
435
451
|
def initialize(conversation:, settings: Settings.new, estimator: TokenEstimator.new, cut_point_finder: CutPointFinder.new(estimator: estimator), file_operation_tracker: FileOperationTracker.new)
|
|
436
452
|
@conversation = conversation
|
|
437
453
|
@settings = settings
|
|
@@ -466,7 +482,7 @@ module Kward
|
|
|
466
482
|
kept_messages: kept_messages,
|
|
467
483
|
turn_prefix_messages: cut.turn_prefix_messages,
|
|
468
484
|
split_turn: cut.split_turn,
|
|
469
|
-
tokens_before: @estimator.context_tokens(@conversation.
|
|
485
|
+
tokens_before: @estimator.context_tokens(@conversation.context_messages),
|
|
470
486
|
previous_summary: previous_entry ? compaction_summary(previous_entry) : nil,
|
|
471
487
|
file_ops: file_ops,
|
|
472
488
|
settings: @settings
|
|
@@ -521,6 +537,7 @@ module Kward
|
|
|
521
537
|
end
|
|
522
538
|
end
|
|
523
539
|
|
|
540
|
+
# Compaction support object used by conversation summarization.
|
|
524
541
|
class PromptBuilder
|
|
525
542
|
SYSTEM_PROMPT = <<~PROMPT.strip.freeze
|
|
526
543
|
You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
|
|
@@ -673,6 +690,7 @@ module Kward
|
|
|
673
690
|
Be concise. Focus only on what is needed to understand and continue from the kept suffix. Preserve exact file paths, commands, class names, module names, method names, constants, spec names, migration names, and error messages.
|
|
674
691
|
PROMPT
|
|
675
692
|
|
|
693
|
+
# Creates an object for conversation compaction.
|
|
676
694
|
def initialize(serializer: ConversationSerializer.new)
|
|
677
695
|
@serializer = serializer
|
|
678
696
|
end
|
|
@@ -728,7 +746,9 @@ module Kward
|
|
|
728
746
|
end
|
|
729
747
|
end
|
|
730
748
|
|
|
749
|
+
# Compaction support object used by conversation summarization.
|
|
731
750
|
class Summarizer
|
|
751
|
+
# Creates an object for conversation compaction.
|
|
732
752
|
def initialize(client:, prompt_builder: PromptBuilder.new)
|
|
733
753
|
@client = client
|
|
734
754
|
@prompt_builder = prompt_builder
|
|
@@ -785,6 +805,7 @@ module Kward
|
|
|
785
805
|
end
|
|
786
806
|
end
|
|
787
807
|
|
|
808
|
+
# Compaction support object used by conversation summarization.
|
|
788
809
|
class Compactor
|
|
789
810
|
Result = Struct.new(:summary, :old_message_count, :new_message_count, :first_kept_entry_id, :tokens_before, :details, keyword_init: true)
|
|
790
811
|
NothingToCompact = Compaction::NothingToCompact
|
|
@@ -795,6 +816,7 @@ module Kward
|
|
|
795
816
|
AUTO_COMPACTION_GUARD_RATIO = 0.10
|
|
796
817
|
AUTO_COMPACTION_EXTRA_GUARD_CAP = 12_000
|
|
797
818
|
|
|
819
|
+
# Creates an object for conversation compaction.
|
|
798
820
|
def initialize(conversation:, client:, tool_result_summarizer: nil, settings: nil, summarizer: nil)
|
|
799
821
|
@conversation = conversation
|
|
800
822
|
@client = client
|
|
@@ -844,7 +866,7 @@ module Kward
|
|
|
844
866
|
context_window ||= @settings.context_window
|
|
845
867
|
return nil unless context_window
|
|
846
868
|
|
|
847
|
-
context_tokens ||= Compaction::TokenEstimator.new.context_tokens(@conversation.
|
|
869
|
+
context_tokens ||= Compaction::TokenEstimator.new.context_tokens(@conversation.context_messages)
|
|
848
870
|
reserve_tokens = auto_compaction_reserve_tokens(context_window: context_window.to_i)
|
|
849
871
|
return nil unless context_tokens.to_i > context_window.to_i - reserve_tokens
|
|
850
872
|
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -5,9 +5,19 @@ require_relative "private_file"
|
|
|
5
5
|
require_relative "prompts/templates"
|
|
6
6
|
require_relative "skills/registry"
|
|
7
7
|
|
|
8
|
+
# Namespace for the Kward CLI agent runtime.
|
|
8
9
|
module Kward
|
|
9
10
|
# Resolves Kward configuration, cache, memory, prompt, skill, and plugin
|
|
10
11
|
# paths, and reads/writes the JSON config file used by the CLI and RPC server.
|
|
12
|
+
#
|
|
13
|
+
# This module is the configuration boundary, not a runtime settings cache.
|
|
14
|
+
# Most methods read the filesystem each time so CLI commands and RPC reloads can
|
|
15
|
+
# observe edits made outside the process. Callers that need caching should own
|
|
16
|
+
# invalidation explicitly, as `Client#reload_config` does for provider state.
|
|
17
|
+
#
|
|
18
|
+
# Keep path decisions here. Higher-level code should ask `ConfigFiles` for
|
|
19
|
+
# config, prompt, skill, plugin, cache, memory, and session locations instead of
|
|
20
|
+
# reconstructing `~/.kward` paths independently.
|
|
11
21
|
module ConfigFiles
|
|
12
22
|
MAX_SKILL_FILE_BYTES = 100_000
|
|
13
23
|
MAX_PROMPT_FILE_BYTES = 32 * 1024
|
|
@@ -67,12 +77,14 @@ module Kward
|
|
|
67
77
|
"sessions" => {
|
|
68
78
|
"auto_resume" => false
|
|
69
79
|
},
|
|
80
|
+
"enforce_workspace_agents_file" => false,
|
|
70
81
|
"tools" => {
|
|
71
82
|
"workspace_guardrails" => true
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
end
|
|
75
86
|
|
|
87
|
+
# Performs ensure default config for configuration file and path handling.
|
|
76
88
|
def ensure_default_config!(path = config_path)
|
|
77
89
|
path = File.expand_path(path)
|
|
78
90
|
return false if File.exist?(path)
|
|
@@ -105,7 +117,9 @@ module Kward
|
|
|
105
117
|
# Reads the JSON config file.
|
|
106
118
|
#
|
|
107
119
|
# Missing files are treated as an empty config. Invalid JSON raises a
|
|
108
|
-
# user-facing error that includes the file path.
|
|
120
|
+
# user-facing error that includes the file path. This method does not merge
|
|
121
|
+
# defaults; callers should apply feature-specific defaults at the point where
|
|
122
|
+
# behavior is decided.
|
|
109
123
|
#
|
|
110
124
|
# @param path [String] config file path
|
|
111
125
|
# @return [Hash] parsed config object
|
|
@@ -126,6 +140,7 @@ module Kward
|
|
|
126
140
|
PrivateFile.write_json(path, config)
|
|
127
141
|
end
|
|
128
142
|
|
|
143
|
+
# Merges top-level config values and writes the updated config privately.
|
|
129
144
|
def update_config(values, path = config_path)
|
|
130
145
|
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
131
146
|
|
|
@@ -135,6 +150,7 @@ module Kward
|
|
|
135
150
|
config
|
|
136
151
|
end
|
|
137
152
|
|
|
153
|
+
# Removes a top-level config key when it exists.
|
|
138
154
|
def delete_config_key(key, path = config_path)
|
|
139
155
|
config = read_config(path)
|
|
140
156
|
existed = config.key?(key.to_s)
|
|
@@ -143,6 +159,7 @@ module Kward
|
|
|
143
159
|
existed
|
|
144
160
|
end
|
|
145
161
|
|
|
162
|
+
# Returns the first present non-empty string value among several config keys.
|
|
146
163
|
def config_value(config, *keys)
|
|
147
164
|
keys.each do |key|
|
|
148
165
|
text = presence(config[key])
|
|
@@ -166,26 +183,43 @@ module Kward
|
|
|
166
183
|
settings
|
|
167
184
|
end
|
|
168
185
|
|
|
186
|
+
# Returns whether the composer should show busy-state keyboard help.
|
|
169
187
|
def composer_busy_help?(config = read_config)
|
|
170
188
|
composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
|
|
171
189
|
composer["busy_help"] != false
|
|
172
190
|
end
|
|
173
191
|
|
|
192
|
+
# Returns whether the terminal startup banner should be displayed.
|
|
174
193
|
def banner_enabled?(config = read_config)
|
|
175
194
|
banner = config["banner"].is_a?(Hash) ? config["banner"] : {}
|
|
176
195
|
banner["enabled"] != false
|
|
177
196
|
end
|
|
178
197
|
|
|
198
|
+
# Returns whether file tools must stay inside the active workspace root.
|
|
179
199
|
def workspace_guardrails_enabled?(config = read_config)
|
|
180
200
|
tools = config["tools"].is_a?(Hash) ? config["tools"] : {}
|
|
181
201
|
tools["workspace_guardrails"] != false
|
|
182
202
|
end
|
|
183
203
|
|
|
204
|
+
# Returns whether new frontends should resume the last active session automatically.
|
|
184
205
|
def session_auto_resume_enabled?(config = read_config)
|
|
185
206
|
sessions = config["sessions"].is_a?(Hash) ? config["sessions"] : {}
|
|
186
207
|
sessions["auto_resume"] == true
|
|
187
208
|
end
|
|
188
209
|
|
|
210
|
+
# Returns whether workspace AGENTS.md contents should be injected directly
|
|
211
|
+
# instead of a compact read-when-relevant instruction.
|
|
212
|
+
def enforce_workspace_agents_file?(config = read_config)
|
|
213
|
+
config["enforce_workspace_agents_file"] == true
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns the nested web-search config object, or an empty config when absent.
|
|
217
|
+
def web_search_config(config = read_config)
|
|
218
|
+
value = config["web_search"]
|
|
219
|
+
value.is_a?(Hash) ? value : {}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Validates and persists terminal overlay settings.
|
|
189
223
|
def update_overlay_settings(values)
|
|
190
224
|
raise "Overlay settings must be an object" unless values.is_a?(Hash)
|
|
191
225
|
|
|
@@ -209,17 +243,34 @@ module Kward
|
|
|
209
243
|
overlay_settings(config)
|
|
210
244
|
end
|
|
211
245
|
|
|
212
|
-
# Reads global
|
|
246
|
+
# Reads global principle instructions from the config directory.
|
|
247
|
+
#
|
|
248
|
+
# `PRINCIPLES.md` is preferred. `AGENTS.md` remains a backwards-compatible
|
|
249
|
+
# alias for existing installations.
|
|
213
250
|
#
|
|
214
251
|
# @return [String, nil] prompt text, or nil when absent/too large
|
|
215
252
|
def agents_prompt
|
|
216
|
-
path =
|
|
217
|
-
read_prompt_file(path, "Kward
|
|
253
|
+
path = config_principles_path
|
|
254
|
+
return read_prompt_file(path, "Kward principles file") if File.exist?(path)
|
|
255
|
+
|
|
256
|
+
read_prompt_file(config_agents_path, "Kward AGENTS.md alias")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def config_principles_path
|
|
260
|
+
File.join(config_dir, "PRINCIPLES.md")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def config_agents_path
|
|
264
|
+
File.join(config_dir, "AGENTS.md")
|
|
218
265
|
end
|
|
219
266
|
|
|
220
267
|
# Builds persona prompt text from default, workspace, model, reasoning,
|
|
221
268
|
# time-of-day, weekday, and suffix config entries.
|
|
222
269
|
#
|
|
270
|
+
# Persona resolution is intentionally data-driven so users can edit config
|
|
271
|
+
# without plugin code. Keep new persona selectors additive and deterministic;
|
|
272
|
+
# prompt construction depends on stable ordering.
|
|
273
|
+
#
|
|
223
274
|
# @param workspace_root [String] active workspace root
|
|
224
275
|
# @param model [String, nil] active model name
|
|
225
276
|
# @param reasoning_effort [String, nil] active reasoning effort
|
|
@@ -235,6 +286,7 @@ module Kward
|
|
|
235
286
|
text
|
|
236
287
|
end
|
|
237
288
|
|
|
289
|
+
# Returns the label of the persona selected by default/workspace/model rules.
|
|
238
290
|
def active_persona_label(workspace_root:, model: nil, config: read_config)
|
|
239
291
|
personas = config["personas"]
|
|
240
292
|
return nil unless personas.is_a?(Hash)
|
|
@@ -267,22 +319,30 @@ module Kward
|
|
|
267
319
|
|
|
268
320
|
characters = crew_characters(personas)
|
|
269
321
|
entries = []
|
|
270
|
-
|
|
271
|
-
add_persona_entry(entries, "default", resolved_persona_text(personas["default"], characters: characters))
|
|
322
|
+
active_persona = { layer: "default", value: personas["default"], name: nil }
|
|
272
323
|
|
|
273
324
|
workspaces = personas["workspaces"]
|
|
274
325
|
if workspaces.is_a?(Hash)
|
|
275
326
|
root = canonical_workspace_root(workspace_root)
|
|
276
327
|
workspaces.each do |path, key|
|
|
277
328
|
if canonical_workspace_root(path) == root
|
|
278
|
-
|
|
329
|
+
active_persona = { layer: "workspace", value: key, name: path }
|
|
279
330
|
break
|
|
280
331
|
end
|
|
281
332
|
end
|
|
282
333
|
end
|
|
283
334
|
|
|
284
335
|
models = personas["models"]
|
|
285
|
-
|
|
336
|
+
if models.is_a?(Hash) && !model.to_s.empty? && models.key?(model.to_s)
|
|
337
|
+
active_persona = { layer: "model", value: models[model.to_s], name: model.to_s }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
add_persona_entry(
|
|
341
|
+
entries,
|
|
342
|
+
active_persona.fetch(:layer),
|
|
343
|
+
resolved_persona_text(active_persona.fetch(:value), characters: characters),
|
|
344
|
+
name: active_persona[:name]
|
|
345
|
+
)
|
|
286
346
|
|
|
287
347
|
modifiers = personas["persona_modifiers"]
|
|
288
348
|
if modifiers.is_a?(Hash)
|
|
@@ -304,10 +364,17 @@ module Kward
|
|
|
304
364
|
|
|
305
365
|
entries
|
|
306
366
|
end
|
|
367
|
+
|
|
368
|
+
def workspace_agents_path(workspace_root)
|
|
369
|
+
File.join(canonical_workspace_root(workspace_root), "AGENTS.md")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def workspace_agents_file?(workspace_root)
|
|
373
|
+
File.exist?(workspace_agents_path(workspace_root))
|
|
374
|
+
end
|
|
375
|
+
|
|
307
376
|
def workspace_agents_prompt(workspace_root)
|
|
308
|
-
|
|
309
|
-
path = File.join(root, "AGENTS.md")
|
|
310
|
-
read_prompt_file(path, "workspace AGENTS.md")
|
|
377
|
+
read_prompt_file(workspace_agents_path(workspace_root), "workspace AGENTS.md")
|
|
311
378
|
end
|
|
312
379
|
|
|
313
380
|
def read_prompt_file(path, label)
|
|
@@ -325,17 +392,6 @@ module Kward
|
|
|
325
392
|
nil
|
|
326
393
|
end
|
|
327
394
|
|
|
328
|
-
def workspace_config(workspace_root, config = read_config)
|
|
329
|
-
workspaces = config["workspaces"]
|
|
330
|
-
return nil unless workspaces.is_a?(Hash)
|
|
331
|
-
|
|
332
|
-
root = canonical_workspace_root(workspace_root)
|
|
333
|
-
workspaces.each do |path, entry|
|
|
334
|
-
return entry if canonical_workspace_root(path) == root
|
|
335
|
-
end
|
|
336
|
-
nil
|
|
337
|
-
end
|
|
338
|
-
|
|
339
395
|
def canonical_workspace_root(path)
|
|
340
396
|
expanded = File.expand_path(path.to_s.empty? ? Dir.pwd : path.to_s)
|
|
341
397
|
File.directory?(expanded) ? File.realpath(expanded) : expanded
|
|
@@ -460,7 +516,6 @@ module Kward
|
|
|
460
516
|
# @return [Array<String>] sorted plugin file paths
|
|
461
517
|
def plugin_paths
|
|
462
518
|
plugins_root = plugin_dir
|
|
463
|
-
warn_legacy_plugin_dir(plugins_root)
|
|
464
519
|
return [] unless Dir.exist?(plugins_root)
|
|
465
520
|
|
|
466
521
|
Dir.glob(File.join(plugins_root, "*.rb")).sort
|
|
@@ -469,17 +524,6 @@ module Kward
|
|
|
469
524
|
[]
|
|
470
525
|
end
|
|
471
526
|
|
|
472
|
-
def warn_legacy_plugin_dir(plugins_root)
|
|
473
|
-
config_path = ENV["KWARD_CONFIG_PATH"]
|
|
474
|
-
return if config_path.to_s.empty?
|
|
475
|
-
|
|
476
|
-
legacy_root = File.expand_path(File.join(File.dirname(config_path), "plugins"))
|
|
477
|
-
return if legacy_root == File.expand_path(plugins_root)
|
|
478
|
-
return unless Dir.exist?(legacy_root)
|
|
479
|
-
|
|
480
|
-
warn "Warning: ignoring Kward plugins in #{legacy_root}; plugins are only loaded from #{File.expand_path(plugins_root)}"
|
|
481
|
-
end
|
|
482
|
-
|
|
483
527
|
# Lists prompt templates exposed as slash commands.
|
|
484
528
|
#
|
|
485
529
|
# @param reserved_commands [Array<String>] command names unavailable to templates
|