yorishiro 0.1.0 → 0.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.yorishirorc.sample +51 -0
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +134 -8
  5. data/docs/en/README.md +97 -2
  6. data/docs/ja/README.md +97 -2
  7. data/docs/ja/how_to_create_skills.md +0 -0
  8. data/docs/ja/ollama_setup.md +148 -0
  9. data/lib/yorishiro/cli.rb +474 -93
  10. data/lib/yorishiro/compactor.rb +53 -0
  11. data/lib/yorishiro/configuration.rb +69 -1
  12. data/lib/yorishiro/conversation.rb +117 -0
  13. data/lib/yorishiro/diff.rb +92 -0
  14. data/lib/yorishiro/hooks.rb +64 -0
  15. data/lib/yorishiro/input_history.rb +49 -0
  16. data/lib/yorishiro/mcp/server_manager.rb +4 -4
  17. data/lib/yorishiro/provider/anthropic.rb +37 -8
  18. data/lib/yorishiro/provider/base.rb +20 -4
  19. data/lib/yorishiro/provider/ollama.rb +70 -45
  20. data/lib/yorishiro/provider/open_ai.rb +21 -5
  21. data/lib/yorishiro/session_resume.rb +73 -0
  22. data/lib/yorishiro/session_store.rb +168 -0
  23. data/lib/yorishiro/skill.rb +11 -0
  24. data/lib/yorishiro/skill_loader.rb +53 -0
  25. data/lib/yorishiro/sub_agent.rb +143 -0
  26. data/lib/yorishiro/tool.rb +7 -0
  27. data/lib/yorishiro/tool_result_cap.rb +31 -0
  28. data/lib/yorishiro/tools/edit_file.rb +87 -0
  29. data/lib/yorishiro/tools/execute_command.rb +8 -0
  30. data/lib/yorishiro/tools/exit_plan_mode.rb +41 -0
  31. data/lib/yorishiro/tools/grep.rb +118 -0
  32. data/lib/yorishiro/tools/list_files.rb +10 -8
  33. data/lib/yorishiro/tools/read_file.rb +27 -9
  34. data/lib/yorishiro/tools/task.rb +74 -0
  35. data/lib/yorishiro/tools/write_file.rb +9 -0
  36. data/lib/yorishiro/version.rb +1 -1
  37. data/lib/yorishiro.rb +13 -1
  38. metadata +33 -5
  39. data/lib/yorishiro/mcp/stdio_transport.rb +0 -85
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yorishiro
4
+ # Caps a tool result before it enters a conversation so a single huge
5
+ # output (a whole file, a big command dump) cannot exhaust a small
6
+ # context window. Scaled to the provider budget when one is known.
7
+ # Shared by the CLI agent loop and subagents.
8
+ module ToolResultCap
9
+ # Tool-result size caps used when the provider reports no context
10
+ # budget (cloud models) and as the floor when it does.
11
+ DEFAULT_TOOL_RESULT_CHARS = 30_000
12
+ MIN_TOOL_RESULT_CHARS = 2_000
13
+
14
+ module_function
15
+
16
+ def cap(output, budget:)
17
+ limit = max_chars(budget)
18
+ return output if output.to_s.length <= limit
19
+
20
+ "#{output[0...limit]}\n... (tool output truncated: showing #{limit} of #{output.length} characters. " \
21
+ "Narrow the request — offset/limit, a glob, or a more specific pattern — to see the rest.)"
22
+ end
23
+
24
+ # One tool result may take at most a quarter of the context budget.
25
+ def max_chars(budget)
26
+ return DEFAULT_TOOL_RESULT_CHARS unless budget
27
+
28
+ [budget * Conversation::CHARS_PER_TOKEN / 4, MIN_TOOL_RESULT_CHARS].max
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yorishiro
4
+ module Tools
5
+ class EditFile < Tool
6
+ def name
7
+ "edit_file"
8
+ end
9
+
10
+ def description
11
+ "Replace an exact string in a file. old_string must match the file content exactly " \
12
+ "(including whitespace and indentation) and must be unique unless replace_all is true."
13
+ end
14
+
15
+ def parameters
16
+ {
17
+ type: "object",
18
+ properties: {
19
+ path: { type: "string", description: "The file path to edit" },
20
+ old_string: { type: "string", description: "The exact text to replace" },
21
+ new_string: { type: "string", description: "The text to replace it with" },
22
+ replace_all: { type: "boolean", description: "Replace all occurrences (default: false)" }
23
+ },
24
+ required: %w[path old_string new_string]
25
+ }
26
+ end
27
+
28
+ def execute(**params)
29
+ path = params[:path] || params["path"]
30
+ old_string = params[:old_string] || params["old_string"]
31
+ new_string = params[:new_string] || params["new_string"]
32
+ replace_all = params[:replace_all] || params["replace_all"] || false
33
+
34
+ raise "File not found: #{path}" unless File.exist?(path)
35
+ raise "Not a file: #{path}" unless File.file?(path)
36
+
37
+ content = File.read(path)
38
+ new_content, count = apply_edit(content, old_string, new_string, replace_all, path)
39
+
40
+ File.write(path, new_content)
41
+ "Successfully replaced #{count} occurrence(s) in #{path}"
42
+ end
43
+
44
+ def permission_check(_arguments)
45
+ :ask
46
+ end
47
+
48
+ def preview(arguments)
49
+ path = arguments[:path] || arguments["path"]
50
+ old_string = arguments[:old_string] || arguments["old_string"]
51
+ new_string = arguments[:new_string] || arguments["new_string"]
52
+ replace_all = arguments[:replace_all] || arguments["replace_all"] || false
53
+ return nil unless path && old_string && new_string
54
+ return nil unless File.file?(path)
55
+
56
+ content = File.read(path)
57
+ new_content, = apply_edit(content, old_string, new_string, replace_all, path)
58
+ Diff.unified(content, new_content, path: path)
59
+ rescue StandardError
60
+ nil
61
+ end
62
+
63
+ private
64
+
65
+ def apply_edit(content, old_string, new_string, replace_all, path)
66
+ raise "old_string and new_string are identical. Provide a different new_string." if old_string == new_string
67
+
68
+ count = content.scan(old_string).length
69
+ if count.zero?
70
+ raise "old_string not found in #{path}. Read the file first and copy the exact text, " \
71
+ "including whitespace and indentation."
72
+ end
73
+ if count > 1 && !replace_all
74
+ raise "old_string appears #{count} times in #{path}. Add surrounding lines to make it unique, " \
75
+ "or set replace_all: true."
76
+ end
77
+
78
+ # Block form so backreference sequences (\1, \\) in new_string are
79
+ # written literally instead of being interpreted.
80
+ new_content = replace_all ? content.gsub(old_string) { new_string } : content.sub(old_string) { new_string }
81
+ [new_content, replace_all ? count : 1]
82
+ rescue ArgumentError => e
83
+ raise "Cannot edit #{path}: #{e.message}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -5,6 +5,13 @@ require "open3"
5
5
  module Yorishiro
6
6
  module Tools
7
7
  class ExecuteCommand < Tool
8
+ # Commands run via `sh -c`, so these characters can chain or inject
9
+ # additional commands (`;`, `&`, `|`, newlines), substitute commands
10
+ # (`$`, backtick, `(`, `)`), or redirect files (`<`, `>`). Commands
11
+ # containing any of them never auto-match allow_commands and always
12
+ # fall back to the interactive permission prompt.
13
+ SHELL_METACHARACTERS = /[;&|`$<>()\n\r]/
14
+
8
15
  def initialize
9
16
  super
10
17
  @allow_commands = []
@@ -62,6 +69,7 @@ module Yorishiro
62
69
 
63
70
  def command_allowed?(command)
64
71
  return true if @session_allowed.include?(command)
72
+ return false if command.match?(SHELL_METACHARACTERS)
65
73
 
66
74
  @allow_commands.any? { |pattern| File.fnmatch(pattern, command) }
67
75
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yorishiro
4
+ module Tools
5
+ # A plan-mode-only tool the model calls to signal that it has finished
6
+ # researching and is ready to present its implementation plan. Calling it
7
+ # is what breaks the plan loop, so the model has an explicit way to stop
8
+ # reading files instead of looping on read-only tools forever.
9
+ class ExitPlanMode < Tool
10
+ def read_only?
11
+ true
12
+ end
13
+
14
+ def name
15
+ "exit_plan_mode"
16
+ end
17
+
18
+ def description
19
+ "Call this tool once you have finished researching and are ready to " \
20
+ "present your implementation plan to the user for approval. Pass the " \
21
+ "complete plan as the `plan` argument. As soon as you understand the " \
22
+ "task, STOP calling read-only tools (read_file, list_files) and call " \
23
+ "this tool instead."
24
+ end
25
+
26
+ def parameters
27
+ {
28
+ type: "object",
29
+ properties: {
30
+ plan: { type: "string", description: "The complete implementation plan as markdown text" }
31
+ },
32
+ required: ["plan"]
33
+ }
34
+ end
35
+
36
+ def execute(**params)
37
+ params[:plan] || params["plan"] || ""
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yorishiro
4
+ module Tools
5
+ class Grep < Tool
6
+ MAX_RESULTS = 100
7
+ EXCLUDED_DIRS = %w[.git node_modules vendor tmp].freeze
8
+ BINARY_CHECK_BYTES = 8_000
9
+ MAX_LINE_LENGTH = 250
10
+
11
+ def read_only?
12
+ true
13
+ end
14
+
15
+ def name
16
+ "grep"
17
+ end
18
+
19
+ def description
20
+ "Search file contents recursively with a Ruby regular expression. " \
21
+ "Returns matches as 'file:line:content'. Hidden files and directories (e.g. .git) are skipped."
22
+ end
23
+
24
+ def parameters
25
+ {
26
+ type: "object",
27
+ properties: {
28
+ pattern: { type: "string", description: "Ruby regular expression to search for" },
29
+ path: { type: "string", description: "Directory to search in (default: current directory)" },
30
+ glob: { type: "string", description: "Glob pattern to filter files (e.g., '*.rb')" }
31
+ },
32
+ required: ["pattern"]
33
+ }
34
+ end
35
+
36
+ def execute(**params)
37
+ pattern = params[:pattern] || params["pattern"]
38
+ path = params[:path] || params["path"] || "."
39
+ glob = params[:glob] || params["glob"]
40
+
41
+ regexp = build_regexp(pattern)
42
+ raise "Directory not found: #{path}" unless Dir.exist?(path)
43
+
44
+ matches, truncated = search(regexp, path, glob)
45
+ format_results(matches, truncated, pattern)
46
+ end
47
+
48
+ private
49
+
50
+ def build_regexp(pattern)
51
+ Regexp.new(pattern)
52
+ rescue RegexpError => e
53
+ raise "Invalid regular expression: #{e.message}"
54
+ end
55
+
56
+ def search(regexp, path, glob)
57
+ matches = []
58
+
59
+ target_files(path, glob).each do |file|
60
+ scan_file(file, regexp) do |line_number, line|
61
+ return [matches, true] if matches.length >= MAX_RESULTS
62
+
63
+ matches << "#{file}:#{line_number}:#{line}"
64
+ end
65
+ end
66
+
67
+ [matches, false]
68
+ end
69
+
70
+ # Dir.glob without FNM_DOTMATCH already skips dotfiles and dot
71
+ # directories; EXCLUDED_DIRS guards against globs that reach into
72
+ # them explicitly (e.g. vendored or dependency directories).
73
+ def target_files(path, glob)
74
+ glob_pattern = File.join(path, glob ? "**/#{glob}" : "**/*")
75
+ Dir.glob(glob_pattern).select do |file|
76
+ File.file?(file) && !excluded?(file, path)
77
+ end
78
+ end
79
+
80
+ # Only path components below the search root count as excluded, so
81
+ # searching inside e.g. /tmp/project still works.
82
+ def excluded?(file, base)
83
+ relative = file.delete_prefix(File.join(base, ""))
84
+ relative.split(File::SEPARATOR).any? { |part| EXCLUDED_DIRS.include?(part) }
85
+ end
86
+
87
+ def scan_file(file, regexp)
88
+ return if binary?(file)
89
+
90
+ File.foreach(file).with_index(1) do |line, line_number|
91
+ line = line.scrub.chomp
92
+ yield line_number, truncate_line(line) if regexp.match?(line)
93
+ end
94
+ rescue SystemCallError
95
+ # Unreadable files are skipped.
96
+ end
97
+
98
+ def binary?(file)
99
+ chunk = File.binread(file, BINARY_CHECK_BYTES)
100
+ chunk&.include?("\0")
101
+ rescue SystemCallError
102
+ true
103
+ end
104
+
105
+ def truncate_line(line)
106
+ line.length > MAX_LINE_LENGTH ? "#{line[0...MAX_LINE_LENGTH]}..." : line
107
+ end
108
+
109
+ def format_results(matches, truncated, pattern)
110
+ return "No matches found for pattern: #{pattern}" if matches.empty?
111
+
112
+ result = matches.join("\n")
113
+ result += "\n... (truncated: showing first #{MAX_RESULTS} matches)" if truncated
114
+ result
115
+ end
116
+ end
117
+ end
118
+ end
@@ -32,14 +32,16 @@ module Yorishiro
32
32
 
33
33
  raise "Directory not found: #{path}" unless Dir.exist?(path)
34
34
 
35
- if pattern
36
- glob_path = File.join(path, pattern)
37
- files = Dir.glob(glob_path)
38
- else
39
- files = Dir.children(path).sort
40
- end
41
-
42
- files.map { |f| File.directory?(f) ? "#{f}/" : f }.join("\n")
35
+ entries = if pattern
36
+ # Glob results already carry the path prefix.
37
+ Dir.glob(File.join(path, pattern)).map { |f| [f, f] }
38
+ else
39
+ # Dir.children returns bare names — resolve them against
40
+ # +path+, not the process cwd, when checking for directories.
41
+ Dir.children(path).sort.map { |name| [name, File.join(path, name)] }
42
+ end
43
+
44
+ entries.map { |display, full| File.directory?(full) ? "#{display}/" : display }.join("\n")
43
45
  end
44
46
  end
45
47
  end
@@ -3,6 +3,12 @@
3
3
  module Yorishiro
4
4
  module Tools
5
5
  class ReadFile < Tool
6
+ # Cap how much of a file one call can return. Small local-model
7
+ # context windows (e.g. Ollama with num_ctx 8192) are exhausted by a
8
+ # single whole-file dump, so large files must be paged instead.
9
+ MAX_LINES = 200
10
+ MAX_LINE_LENGTH = 500
11
+
6
12
  def read_only?
7
13
  true
8
14
  end
@@ -12,7 +18,8 @@ module Yorishiro
12
18
  end
13
19
 
14
20
  def description
15
- "Read the contents of a file at the specified path. Optionally specify offset and limit for partial reads."
21
+ "Read the contents of a file at the specified path. Returns at most #{MAX_LINES} lines per call; " \
22
+ "use offset and limit to page through larger files."
16
23
  end
17
24
 
18
25
  def parameters
@@ -21,7 +28,7 @@ module Yorishiro
21
28
  properties: {
22
29
  path: { type: "string", description: "The file path to read" },
23
30
  offset: { type: "integer", description: "Line number to start reading from (0-based)" },
24
- limit: { type: "integer", description: "Maximum number of lines to read" }
31
+ limit: { type: "integer", description: "Maximum number of lines to read (capped at #{MAX_LINES})" }
25
32
  },
26
33
  required: ["path"]
27
34
  }
@@ -29,21 +36,32 @@ module Yorishiro
29
36
 
30
37
  def execute(**params)
31
38
  path = params[:path] || params["path"]
32
- offset = (params[:offset] || params["offset"])&.to_i
39
+ offset = (params[:offset] || params["offset"]).to_i
33
40
  limit = (params[:limit] || params["limit"])&.to_i
41
+ limit = limit.nil? ? MAX_LINES : [limit, MAX_LINES].min
34
42
 
35
43
  raise "File not found: #{path}" unless File.exist?(path)
36
44
  raise "Not a file: #{path}" unless File.file?(path)
37
45
 
38
46
  lines = File.readlines(path)
47
+ selected = lines[offset, limit] || []
48
+
49
+ output = selected.each_with_index.map { |line, i| "#{offset + i + 1}: #{truncate_line(line)}" }.join
50
+ output + paging_notice(lines.length, offset, selected.length)
51
+ end
52
+
53
+ private
54
+
55
+ def truncate_line(line)
56
+ return line if line.chomp.length <= MAX_LINE_LENGTH
57
+
58
+ "#{line.chomp[0...MAX_LINE_LENGTH]}... (line truncated)\n"
59
+ end
39
60
 
40
- if offset || limit
41
- offset ||= 0
42
- limit ||= lines.length
43
- lines = lines[offset, limit] || []
44
- end
61
+ def paging_notice(total, offset, shown)
62
+ return "" unless total > offset + shown
45
63
 
46
- lines.each_with_index.map { |line, i| "#{(offset || 0) + i + 1}: #{line}" }.join
64
+ "... (file has #{total} lines; showing #{offset + 1}-#{offset + shown}. Use offset/limit to read more.)"
47
65
  end
48
66
  end
49
67
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yorishiro
4
+ module Tools
5
+ class Task < Tool
6
+ # Read-only: the subagent only ever receives read-only tools, so a
7
+ # task can never mutate anything.
8
+ def read_only?
9
+ true
10
+ end
11
+
12
+ def name
13
+ "task"
14
+ end
15
+
16
+ def description
17
+ "Delegate a read-only research task to a subagent with its own fresh " \
18
+ "context window. The subagent can read files, list files, and grep, " \
19
+ "and returns a text summary of its findings. Use it for exploratory " \
20
+ "work (finding where something is defined, summarizing several files) " \
21
+ "to keep your own context small. The subagent cannot see this " \
22
+ "conversation, so the prompt must be complete and self-contained."
23
+ end
24
+
25
+ def parameters
26
+ {
27
+ type: "object",
28
+ properties: {
29
+ description: {
30
+ type: "string",
31
+ description: "Short (3-7 word) label for the task"
32
+ },
33
+ prompt: {
34
+ type: "string",
35
+ description: "Complete, self-contained instructions: what to investigate and what to report back"
36
+ }
37
+ },
38
+ required: ["prompt"]
39
+ }
40
+ end
41
+
42
+ # Called by the CLI so the subagent reuses the session's provider and
43
+ # prints its progress to the session's output.
44
+ def attach(provider:, output:)
45
+ @provider = provider
46
+ @output = output
47
+ end
48
+
49
+ def execute(**params)
50
+ prompt = params[:prompt] || params["prompt"]
51
+ raise "prompt is required" if prompt.to_s.strip.empty?
52
+
53
+ SubAgent.new(provider: provider, tools: child_tools, output: output).run(prompt)
54
+ end
55
+
56
+ private
57
+
58
+ def provider
59
+ @provider ||= Provider.build(Yorishiro.configuration)
60
+ end
61
+
62
+ def output
63
+ @output || $stdout
64
+ end
65
+
66
+ # Read-only tools never prompt for permission, so the subagent loop
67
+ # runs unattended. The task tool itself is excluded so subagents
68
+ # cannot nest.
69
+ def child_tools
70
+ Yorishiro.configuration.allowed_tools.select(&:read_only?).grep_v(Task)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -36,6 +36,15 @@ module Yorishiro
36
36
  def permission_check(_arguments)
37
37
  :ask
38
38
  end
39
+
40
+ def preview(arguments)
41
+ path = arguments[:path] || arguments["path"]
42
+ content = arguments[:content] || arguments["content"]
43
+ return nil unless path && content
44
+
45
+ old_content = File.file?(path) ? File.read(path) : ""
46
+ Diff.unified(old_content, content, path: path)
47
+ end
39
48
  end
40
49
  end
41
50
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yorishiro
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/yorishiro.rb CHANGED
@@ -6,18 +6,30 @@ require "securerandom"
6
6
 
7
7
  require_relative "yorishiro/version"
8
8
  require_relative "yorishiro/tool"
9
+ require_relative "yorishiro/diff"
9
10
  require_relative "yorishiro/skill"
11
+ require_relative "yorishiro/skill_loader"
12
+ require_relative "yorishiro/hooks"
10
13
  require_relative "yorishiro/configuration"
11
14
  require_relative "yorishiro/conversation"
15
+ require_relative "yorishiro/tool_result_cap"
16
+ require_relative "yorishiro/input_history"
17
+ require_relative "yorishiro/session_store"
18
+ require_relative "yorishiro/session_resume"
19
+ require_relative "yorishiro/compactor"
12
20
  require_relative "yorishiro/provider/base"
13
21
  require_relative "yorishiro/provider/anthropic"
14
22
  require_relative "yorishiro/provider/open_ai"
15
23
  require_relative "yorishiro/provider/ollama"
24
+ require_relative "yorishiro/sub_agent"
16
25
  require_relative "yorishiro/tools/read_file"
17
26
  require_relative "yorishiro/tools/write_file"
27
+ require_relative "yorishiro/tools/edit_file"
18
28
  require_relative "yorishiro/tools/list_files"
29
+ require_relative "yorishiro/tools/grep"
19
30
  require_relative "yorishiro/tools/execute_command"
20
- require_relative "yorishiro/mcp/stdio_transport"
31
+ require_relative "yorishiro/tools/exit_plan_mode"
32
+ require_relative "yorishiro/tools/task"
21
33
  require_relative "yorishiro/mcp/tool"
22
34
  require_relative "yorishiro/mcp/server_manager"
23
35
  require_relative "yorishiro/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yorishiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: json
13
+ name: base64
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -24,7 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: mcp
27
+ name: json
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mcp
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.22'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.22'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: net-http
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -96,23 +110,37 @@ files:
96
110
  - Rakefile
97
111
  - docs/en/README.md
98
112
  - docs/ja/README.md
113
+ - docs/ja/how_to_create_skills.md
114
+ - docs/ja/ollama_setup.md
99
115
  - exe/yorishiro
100
116
  - lib/yorishiro.rb
101
117
  - lib/yorishiro/cli.rb
118
+ - lib/yorishiro/compactor.rb
102
119
  - lib/yorishiro/configuration.rb
103
120
  - lib/yorishiro/conversation.rb
121
+ - lib/yorishiro/diff.rb
122
+ - lib/yorishiro/hooks.rb
123
+ - lib/yorishiro/input_history.rb
104
124
  - lib/yorishiro/mcp/server_manager.rb
105
- - lib/yorishiro/mcp/stdio_transport.rb
106
125
  - lib/yorishiro/mcp/tool.rb
107
126
  - lib/yorishiro/provider/anthropic.rb
108
127
  - lib/yorishiro/provider/base.rb
109
128
  - lib/yorishiro/provider/ollama.rb
110
129
  - lib/yorishiro/provider/open_ai.rb
130
+ - lib/yorishiro/session_resume.rb
131
+ - lib/yorishiro/session_store.rb
111
132
  - lib/yorishiro/skill.rb
133
+ - lib/yorishiro/skill_loader.rb
134
+ - lib/yorishiro/sub_agent.rb
112
135
  - lib/yorishiro/tool.rb
136
+ - lib/yorishiro/tool_result_cap.rb
137
+ - lib/yorishiro/tools/edit_file.rb
113
138
  - lib/yorishiro/tools/execute_command.rb
139
+ - lib/yorishiro/tools/exit_plan_mode.rb
140
+ - lib/yorishiro/tools/grep.rb
114
141
  - lib/yorishiro/tools/list_files.rb
115
142
  - lib/yorishiro/tools/read_file.rb
143
+ - lib/yorishiro/tools/task.rb
116
144
  - lib/yorishiro/tools/write_file.rb
117
145
  - lib/yorishiro/version.rb
118
146
  - sig/yorishiro.rbs
@@ -139,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
167
  - !ruby/object:Gem::Version
140
168
  version: '0'
141
169
  requirements: []
142
- rubygems_version: 4.1.0.dev
170
+ rubygems_version: 4.0.16
143
171
  specification_version: 4
144
172
  summary: A Ruby CLI LLM agent with tool execution, MCP support, and multi-provider
145
173
  backends