ask-tools-shell 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df1f5ba13ed96fd946f5fae6eaa010ac2ae9c8ef6c87a48fcec7ec0b994f1164
4
+ data.tar.gz: 79a8c50c47d6b43ba2fbe9dddd8a514aac6f2baa5951c240131003f4f88ac22d
5
+ SHA512:
6
+ metadata.gz: d918ba9bd43e27f511c82f9adc4de3d6daa8817531555cfc567729f807f92df8e18f28932aa6e59738c0d2f1e653562255240df5974f3e128249e8ac04258418
7
+ data.tar.gz: 226ea5d605b60d10a8b6839c4469d5cbc97076f3c08596e59c0adb0fe29ed0b03bc95b54261af51a3c6babd074489a9b0ca3b55f47281d0cd8a4d1f41e01fe24
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # ask-tools-shell
2
+
3
+ Shell, filesystem, and code execution tools for AI agents. Part of the ask-rb ecosystem.
4
+
5
+ Provides **Bash**, **Read**, **Write**, **Edit**, **Glob**, **Grep**, and **Code** — the execution tools every agent needs.
6
+
7
+ ```ruby
8
+ gem "ask-tools-shell"
9
+ ```
10
+
11
+ ## Dependencies
12
+
13
+ - **ask-tools** ~> 0.1 (provides `Ask::Tool` base class and `Ask::Result`)
14
+
15
+ ---
16
+
17
+ ## Quick Start
18
+
19
+ ```ruby
20
+ require "ask-tools-shell"
21
+
22
+ # List all available tools
23
+ Ask::Tools::Shell.all.map(&:name)
24
+ # => ["bash", "read", "write", "edit", "glob", "grep", "code"]
25
+
26
+ # Use a tool standalone
27
+ result = Ask::Tools::Bash.new.call(command: "echo hello")
28
+ result.ok? # => true
29
+ result.output[:stdout] # => "hello\n"
30
+ result.output[:exit_code] # => 0
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Tools
36
+
37
+ ### `Ask::Tools::Bash`
38
+
39
+ Execute shell commands in a sandboxed temp directory.
40
+
41
+ | Param | Type | Required | Default | Description |
42
+ |-------|------|----------|---------|-------------|
43
+ | `command` | `string` | Yes | — | The bash command to execute |
44
+ | `timeout` | `integer` | No | 30 | Timeout in seconds |
45
+ | `workdir` | `string` | No | temp dir | Working directory |
46
+
47
+ Returns `{ stdout, stderr, exit_code, timed_out }`. Output truncated to 100KB. Process killed on timeout.
48
+
49
+ ```ruby
50
+ Ask::Tools::Bash.new.call(command: "ls -la", timeout: 10)
51
+ ```
52
+
53
+ ### `Ask::Tools::Read`
54
+
55
+ Read file contents with line numbers, or list directory entries.
56
+
57
+ | Param | Type | Required | Default | Description |
58
+ |-------|------|----------|---------|-------------|
59
+ | `path` | `string` | Yes | — | Absolute path to file or directory |
60
+ | `offset` | `integer` | No | 0 | Starting line number (0-indexed) |
61
+ | `limit` | `integer` | No | 2000 | Maximum lines to read |
62
+
63
+ ```ruby
64
+ Ask::Tools::Read.new.call(path: "/etc/hosts")
65
+ Ask::Tools::Read.new.call(path: "large.log", offset: 100, limit: 50)
66
+ ```
67
+
68
+ ### `Ask::Tools::Write`
69
+
70
+ Write content to a file. Creates parent directories automatically.
71
+
72
+ | Param | Type | Required | Default | Description |
73
+ |-------|------|----------|---------|-------------|
74
+ | `path` | `string` | Yes | — | Absolute path to write to |
75
+ | `content` | `string` | Yes | — | File content (max 500KB) |
76
+
77
+ ```ruby
78
+ Ask::Tools::Write.new.call(path: "/tmp/hello.txt", content: "Hello, World!")
79
+ ```
80
+
81
+ ### `Ask::Tools::Edit`
82
+
83
+ Replace exact text in a file. Uses exact string matching.
84
+
85
+ | Param | Type | Required | Default | Description |
86
+ |-------|------|----------|---------|-------------|
87
+ | `path` | `string` | Yes | — | Absolute path to the file |
88
+ | `old_string` | `string` | Yes | — | Exact text to replace |
89
+ | `new_string` | `string` | Yes | — | Replacement text |
90
+ | `replace_all` | `boolean` | No | false | Replace all occurrences |
91
+
92
+ ```ruby
93
+ Ask::Tools::Edit.new.call(path: "file.rb", old_string: "foo", new_string: "bar")
94
+ Ask::Tools::Edit.new.call(path: "file.rb", old_string: "x", new_string: "y", replace_all: true)
95
+ ```
96
+
97
+ ### `Ask::Tools::Glob`
98
+
99
+ Find files matching a glob pattern, sorted by modification time (newest first).
100
+
101
+ | Param | Type | Required | Default | Description |
102
+ |-------|------|----------|---------|-------------|
103
+ | `pattern` | `string` | Yes | — | Glob pattern (e.g. `**/*.rb`) |
104
+ | `path` | `string` | No | current dir | Base directory |
105
+
106
+ Max 1000 results.
107
+
108
+ ```ruby
109
+ Ask::Tools::Glob.new.call(pattern: "**/*.rb", path: "/path/to/project")
110
+ ```
111
+
112
+ ### `Ask::Tools::Grep`
113
+
114
+ Search file contents using a regex pattern.
115
+
116
+ | Param | Type | Required | Default | Description |
117
+ |-------|------|----------|---------|-------------|
118
+ | `pattern` | `string` | Yes | — | Regex pattern to search for |
119
+ | `path` | `string` | No | current dir | Directory to search |
120
+ | `include` | `string` | No | `**/*` | File pattern filter (e.g. `*.rb`) |
121
+
122
+ Max 100 matches. Line content capped at 500 chars. Skips `.git`, `node_modules`, `vendor`, `.bundle`, `tmp`, `log`.
123
+
124
+ ```ruby
125
+ Ask::Tools::Grep.new.call(pattern: "TODO", path: ".", include: "*.rb")
126
+ ```
127
+
128
+ ### `Ask::Tools::Code`
129
+
130
+ Write and execute Ruby code in a subprocess. Uses gems already available in the environment.
131
+
132
+ | Param | Type | Required | Default | Description |
133
+ |-------|------|----------|---------|-------------|
134
+ | `code` | `string` | Yes | — | Ruby source code to execute |
135
+
136
+ Returns `{ stdout, stderr, exit_code }`. Output truncated to 100KB.
137
+
138
+ ```ruby
139
+ Ask::Tools::Code.new.call(code: <<~RUBY)
140
+ puts "Hello from Ruby!"
141
+ result = 2 + 2
142
+ puts "2 + 2 = #{result}"
143
+ RUBY
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Using Tools with an Agent
149
+
150
+ ```ruby
151
+ require "ask-tools-shell"
152
+
153
+ # All tools
154
+ tools = Ask::Tools::Shell.all
155
+
156
+ # Find by name
157
+ bash = Ask::Tools["bash"]
158
+ bash.call(command: "date")
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ bundle install
167
+ bundle exec rake test
168
+ gem build ask-tools-shell.gemspec
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "tempfile"
5
+ require "stringio"
6
+ require "tmpdir"
7
+
8
+ module Ask
9
+ module Tools
10
+ # Execute shell commands in a sandboxed environment.
11
+ # Returns stdout, stderr, exit code, and a timed_out flag.
12
+ # Output is truncated to 100KB.
13
+ class Bash < Ask::Tool
14
+ description "Execute a bash command in a sandboxed environment. " \
15
+ "Returns stdout, stderr, and exit code. " \
16
+ "Output is truncated to 100KB."
17
+
18
+ param :command, type: :string, desc: "The bash command to execute", required: true
19
+ param :timeout, type: :integer, desc: "Timeout in seconds", required: false
20
+ param :workdir, type: :string, desc: "Working directory", required: false
21
+
22
+ MAX_OUTPUT_SIZE = 102_400
23
+
24
+ def execute(command:, timeout: 30, workdir: nil)
25
+ Dir.mktmpdir("ask_bash") do |dir|
26
+ workdir ||= dir
27
+
28
+ stdout = StringIO.new
29
+ stderr = StringIO.new
30
+ timed_out = false
31
+ exit_code = -1
32
+
33
+ begin
34
+ Open3.popen3("bash", "-c", command, chdir: workdir) do |stdin, out, err, wait_thr|
35
+ stdin.close
36
+
37
+ threads = [
38
+ Thread.new { IO.copy_stream(out, stdout) rescue nil },
39
+ Thread.new { IO.copy_stream(err, stderr) rescue nil }
40
+ ]
41
+
42
+ unless wait_thr.join(timeout)
43
+ Process.kill("-KILL", wait_thr.pid) rescue nil
44
+ timed_out = true
45
+ end
46
+
47
+ threads.each(&:join)
48
+ exit_code = timed_out ? -1 : wait_thr.value.exitstatus
49
+ end
50
+ rescue => e
51
+ return Ask::Result.error(message: "Bash execution failed: #{e.message}",
52
+ metadata: { stdout: stdout.string, stderr: stderr.string })
53
+ end
54
+
55
+ out_text = stdout.string
56
+ err_text = stderr.string
57
+
58
+ if out_text.length > MAX_OUTPUT_SIZE
59
+ header = "[Output truncated to #{MAX_OUTPUT_SIZE / 1024}KB]\n"
60
+ out_text = "#{header}#{out_text[-(MAX_OUTPUT_SIZE - header.length)..]}"
61
+ end
62
+
63
+ if err_text.length > MAX_OUTPUT_SIZE
64
+ header = "[Error output truncated to #{MAX_OUTPUT_SIZE / 1024}KB]\n"
65
+ err_text = "#{header}#{err_text[-(MAX_OUTPUT_SIZE - header.length)..]}"
66
+ end
67
+
68
+ Ask::Result.ok(data: {
69
+ stdout: out_text,
70
+ stderr: err_text,
71
+ exit_code: exit_code,
72
+ timed_out: timed_out
73
+ })
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "tmpdir"
5
+
6
+ module Ask
7
+ module Tools
8
+ # Write and execute Ruby code in a subprocess.
9
+ # Runs in a temp directory via ruby -e.
10
+ class Code < Ask::Tool
11
+ description "Write and execute Ruby code in a subprocess. " \
12
+ "Returns stdout, stderr, and exit code. " \
13
+ "Uses gems already available in the environment."
14
+
15
+ param :code, type: :string, desc: "Ruby source code to execute", required: true
16
+
17
+ MAX_OUTPUT_SIZE = 102_400
18
+
19
+ def execute(code:)
20
+ Dir.mktmpdir("ask_code") do |_dir|
21
+ stdout = StringIO.new
22
+ stderr = StringIO.new
23
+ exit_code = -1
24
+
25
+ begin
26
+ Open3.popen3("ruby", "-e", code, chdir: Dir.pwd) do |stdin, out, err, wait_thr|
27
+ stdin.close
28
+
29
+ threads = [
30
+ Thread.new { IO.copy_stream(out, stdout) rescue nil },
31
+ Thread.new { IO.copy_stream(err, stderr) rescue nil }
32
+ ]
33
+
34
+ threads.each(&:join)
35
+ exit_code = wait_thr.value.exitstatus
36
+ end
37
+ rescue => e
38
+ return Ask::Result.error(message: "Code execution failed: #{e.message}",
39
+ metadata: { stdout: stdout.string, stderr: stderr.string })
40
+ end
41
+
42
+ out_text = stdout.string
43
+ err_text = stderr.string
44
+
45
+ if out_text.length > MAX_OUTPUT_SIZE
46
+ header = "[Output truncated to #{MAX_OUTPUT_SIZE / 1024}KB]\n"
47
+ out_text = "#{header}#{out_text[-(MAX_OUTPUT_SIZE - header.length)..]}"
48
+ end
49
+
50
+ Ask::Result.ok(data: {
51
+ stdout: out_text,
52
+ stderr: err_text,
53
+ exit_code: exit_code
54
+ })
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ # Edit a file by replacing exact text.
6
+ # Uses exact string matching — provide enough surrounding context for uniqueness.
7
+ class Edit < Ask::Tool
8
+ description "Edit a file by replacing exact text. " \
9
+ "Uses exact string matching — provide enough surrounding context for uniqueness."
10
+
11
+ param :path, type: :string, desc: "Absolute path to the file", required: true
12
+ param :old_string, type: :string, desc: "Exact text to replace", required: true
13
+ param :new_string, type: :string, desc: "Replacement text", required: true
14
+ param :replace_all, type: :boolean, desc: "Replace all occurrences if true", required: false
15
+
16
+ MAX_FILE_SIZE = 1_000_000
17
+
18
+ def execute(path:, old_string:, new_string:, replace_all: false)
19
+ path = File.expand_path(path)
20
+
21
+ unless File.exist?(path)
22
+ return Ask::Result.error(message: "File does not exist: #{path}")
23
+ end
24
+
25
+ unless File.file?(path)
26
+ return Ask::Result.error(message: "Not a file: #{path}")
27
+ end
28
+
29
+ if File.size(path) > MAX_FILE_SIZE
30
+ return Ask::Result.error(
31
+ message: "File too large (#{File.size(path)} bytes). Maximum is #{MAX_FILE_SIZE} bytes."
32
+ )
33
+ end
34
+
35
+ content = File.read(path)
36
+
37
+ if replace_all
38
+ count = content.scan(old_string).size
39
+ return Ask::Result.error(message: "String not found") if count == 0
40
+ content = content.gsub(old_string, new_string)
41
+ else
42
+ index = content.index(old_string)
43
+ return Ask::Result.error(message: "String not found") unless index
44
+ count = 1
45
+ content = content.sub(old_string, new_string)
46
+ end
47
+
48
+ File.write(path, content)
49
+ Ask::Result.ok(data: { path: path, replacements: count },
50
+ metadata: { path: path, replacements: count })
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ # Find files matching a glob pattern, newest first.
6
+ class Glob < Ask::Tool
7
+ description "Find files matching a glob pattern. " \
8
+ "Returns up to 1000 matching file paths sorted by modification time (newest first)."
9
+
10
+ param :pattern, type: :string, desc: "Glob pattern (e.g. **/*.rb)", required: true
11
+ param :path, type: :string, desc: "Base directory (default: current)", required: false
12
+
13
+ MAX_RESULTS = 1000
14
+
15
+ def execute(pattern:, path: nil)
16
+ base = path ? File.expand_path(path) : Dir.pwd
17
+
18
+ unless File.directory?(base)
19
+ return Ask::Result.error(message: "Directory does not exist: #{base}")
20
+ end
21
+
22
+ files = Dir.glob(pattern, base: base).map { |f| File.join(base, f) }
23
+ files = files.select { |f| File.file?(f) }
24
+ files = files.sort_by { |f| -File.mtime(f).to_i }
25
+ files = files.first(MAX_RESULTS)
26
+
27
+ if files.empty?
28
+ return Ask::Result.error(message: "No files found matching: #{pattern}")
29
+ end
30
+
31
+ result = files.join("\n")
32
+ result << "\n... (#{files.size} files shown)" if files.size == MAX_RESULTS
33
+
34
+ Ask::Result.ok(data: result, metadata: { count: files.size, truncated: files.size == MAX_RESULTS })
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ # Search file contents using a regex pattern.
6
+ class Grep < Ask::Tool
7
+ description "Search file contents using a regex pattern. " \
8
+ "Returns matching file paths with line numbers and content. " \
9
+ "Truncated to 100 matches with line content capped at 500 chars."
10
+
11
+ param :pattern, type: :string, desc: "Regex pattern to search for", required: true
12
+ param :path, type: :string, desc: "Directory to search in (default: current)", required: false
13
+ param :include, type: :string, desc: "File pattern filter (e.g. *.rb)", required: false
14
+
15
+ MAX_MATCHES = 100
16
+ MAX_LINE_LENGTH = 500
17
+ EXCLUDE_DIRS = %w[.git node_modules vendor .bundle tmp log].freeze
18
+
19
+ def execute(pattern:, path: nil, include: nil)
20
+ base = path ? File.expand_path(path) : Dir.pwd
21
+
22
+ unless File.directory?(base)
23
+ return Ask::Result.error(message: "Directory does not exist: #{base}")
24
+ end
25
+
26
+ begin
27
+ regex = Regexp.new(pattern, Regexp::IGNORECASE)
28
+ rescue RegexpError => e
29
+ return Ask::Result.error(message: "Invalid regex: #{e.message}")
30
+ end
31
+
32
+ matches = []
33
+ glob = include || "**/*"
34
+
35
+ Dir.glob(File.join(base, glob)).each do |file|
36
+ next unless File.file?(file)
37
+ next if EXCLUDE_DIRS.any? { |d| file.include?("/#{d}/") }
38
+
39
+ begin
40
+ File.readlines(file).each_with_index do |line, i|
41
+ next unless regex.match?(line)
42
+ line_text = line.chomp
43
+ line_text = line_text[0, MAX_LINE_LENGTH] + "..." if line_text.length > MAX_LINE_LENGTH
44
+ matches << "#{file}:#{i + 1}: #{line_text}"
45
+ if matches.size >= MAX_MATCHES
46
+ return Ask::Result.ok(data: matches.join("\n") + "\n... (too many matches)",
47
+ metadata: { count: matches.size, truncated: true })
48
+ end
49
+ end
50
+ rescue => e
51
+ matches << "#{file}: error reading: #{e.message}"
52
+ end
53
+ end
54
+
55
+ if matches.empty?
56
+ return Ask::Result.error(message: "No matches found for: #{pattern}")
57
+ end
58
+
59
+ Ask::Result.ok(data: matches.join("\n"), metadata: { count: matches.size, truncated: false })
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ask
6
+ module Tools
7
+ # Read file contents with line numbers, or list directory contents.
8
+ # Output is truncated to 2000 lines.
9
+ class Read < Ask::Tool
10
+ description "Read the contents of a file or list a directory. " \
11
+ "Files are displayed with line numbers. " \
12
+ "Output is truncated to 2000 lines."
13
+
14
+ param :path, type: :string, desc: "Absolute path to the file or directory", required: true
15
+ param :offset, type: :integer, desc: "Starting line number (0-indexed)", required: false
16
+ param :limit, type: :integer, desc: "Maximum number of lines to read", required: false
17
+
18
+ MAX_LINES = 2000
19
+
20
+ def execute(path:, offset: nil, limit: nil)
21
+ path = File.expand_path(path)
22
+
23
+ unless File.exist?(path)
24
+ return Ask::Result.error(message: "Path does not exist: #{path}")
25
+ end
26
+
27
+ if File.directory?(path)
28
+ entries = Dir.children(path).sort
29
+ entries.map! do |e|
30
+ full = File.join(path, e)
31
+ "#{e}#{File.directory?(full) ? '/' : ''}"
32
+ end
33
+ return Ask::Result.ok(data: entries.join("\n"), metadata: { type: "directory", count: entries.size })
34
+ end
35
+
36
+ unless File.file?(path)
37
+ return Ask::Result.error(message: "Not a file: #{path}")
38
+ end
39
+
40
+ if File.size(path) > 1_000_000
41
+ return Ask::Result.error(
42
+ message: "File too large (#{File.size(path)} bytes). Use offset/limit to read portions."
43
+ )
44
+ end
45
+
46
+ lines = File.readlines(path, chomp: true)
47
+ total = lines.size
48
+ offset_val = offset.to_i.clamp(0, total)
49
+ limit_val = limit || MAX_LINES
50
+
51
+ selected = lines[offset_val, limit_val]
52
+ truncated = selected.size < (total - offset_val)
53
+
54
+ result = selected.each_with_index.map do |line, i|
55
+ "#{offset_val + i + 1}: #{line}"
56
+ end.join("\n")
57
+
58
+ result << "\n... (#{total - offset_val - selected.size} more lines)" if truncated
59
+
60
+ Ask::Result.ok(data: result, metadata: {
61
+ total_lines: total,
62
+ start_line: offset_val + 1,
63
+ end_line: offset_val + selected.size,
64
+ truncated: truncated
65
+ })
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ module Shell
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ask
6
+ module Tools
7
+ # Write content to a file, creating parent directories automatically.
8
+ class Write < Ask::Tool
9
+ description "Write content to a file at the specified path. " \
10
+ "Creates parent directories automatically. " \
11
+ "Returns the path and number of bytes written."
12
+
13
+ param :path, type: :string, desc: "Absolute path to write to", required: true
14
+ param :content, type: :string, desc: "File content to write", required: true
15
+
16
+ MAX_CONTENT_SIZE = 500_000
17
+
18
+ def execute(path:, content:)
19
+ path = File.expand_path(path)
20
+
21
+ if content.length > MAX_CONTENT_SIZE
22
+ return Ask::Result.error(
23
+ message: "Content too large (#{content.length} bytes). Maximum is #{MAX_CONTENT_SIZE} bytes."
24
+ )
25
+ end
26
+
27
+ dir = File.dirname(path)
28
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
29
+
30
+ bytes = File.write(path, content)
31
+ Ask::Result.ok(data: { path: path, bytes: bytes },
32
+ metadata: { path: path, bytes: bytes })
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ # Collection point for shell tools.
6
+ #
7
+ # Ask::Tools::Shell.all # => [Bash, Read, Write, ...] instances
8
+ #
9
+ module Shell
10
+ TOOLS = [Bash, Read, Write, Edit, Glob, Grep, Code].freeze
11
+
12
+ # Return an instance of every registered shell tool.
13
+ #
14
+ # @return [Array<Ask::Tool>]
15
+ def self.all
16
+ TOOLS.map(&:new)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask-tools"
4
+
5
+ require_relative "ask/tools/shell/version"
6
+ require_relative "ask/tools/shell/bash"
7
+ require_relative "ask/tools/shell/read"
8
+ require_relative "ask/tools/shell/write"
9
+ require_relative "ask/tools/shell/edit"
10
+ require_relative "ask/tools/shell/glob"
11
+ require_relative "ask/tools/shell/grep"
12
+ require_relative "ask/tools/shell/code"
13
+ require_relative "ask/tools/shell"
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-tools-shell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ask-tools
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.25'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mocha
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ description: Bash, Read, Write, Edit, Glob, Grep, and Code tools for the ask-rb ecosystem.
69
+ email:
70
+ - kaka@myrrlabs.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - LICENSE
76
+ - README.md
77
+ - lib/ask-tools-shell.rb
78
+ - lib/ask/tools/shell.rb
79
+ - lib/ask/tools/shell/bash.rb
80
+ - lib/ask/tools/shell/code.rb
81
+ - lib/ask/tools/shell/edit.rb
82
+ - lib/ask/tools/shell/glob.rb
83
+ - lib/ask/tools/shell/grep.rb
84
+ - lib/ask/tools/shell/read.rb
85
+ - lib/ask/tools/shell/version.rb
86
+ - lib/ask/tools/shell/write.rb
87
+ homepage: https://github.com/ask-rb/ask-tools-shell
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '3.2'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 4.0.3
106
+ specification_version: 4
107
+ summary: Shell, filesystem, and code execution tools
108
+ test_files: []