ask-tools-shell 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df1f5ba13ed96fd946f5fae6eaa010ac2ae9c8ef6c87a48fcec7ec0b994f1164
4
- data.tar.gz: 79a8c50c47d6b43ba2fbe9dddd8a514aac6f2baa5951c240131003f4f88ac22d
3
+ metadata.gz: 57dd21c05244ce8dfcdc2b9c631ed37983b0d87071421906c7a38b090a36d237
4
+ data.tar.gz: 786e6bb78c5f98840d45554d77c826ddec8c94320fba1696425833e865bfef6e
5
5
  SHA512:
6
- metadata.gz: d918ba9bd43e27f511c82f9adc4de3d6daa8817531555cfc567729f807f92df8e18f28932aa6e59738c0d2f1e653562255240df5974f3e128249e8ac04258418
7
- data.tar.gz: 226ea5d605b60d10a8b6839c4469d5cbc97076f3c08596e59c0adb0fe29ed0b03bc95b54261af51a3c6babd074489a9b0ca3b55f47281d0cd8a4d1f41e01fe24
6
+ metadata.gz: cc2b723b0e12766e682ad46cebe01b60cd35fbc8df1fbece1c165f03f05bdfd3b534745cb57af78dac6bf550130a6cc21431f67f31d640ce025cfef7ec91b300
7
+ data.tar.gz: 7f5bf812384a63539c90b153d3bb1e5bfc30014634f357ce49bbf4f8860fbb423ac0dda9e813da12f50e0a0d6c3614e7b8a95dfaee95c738674bfcd53a3e4ec3
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: shell.patterns
3
+ description: Common shell tool composition patterns for code analysis, searching, and file manipulation
4
+ ---
5
+
6
+ Use this skill when you need to search, analyze, or manipulate a codebase using
7
+ shell tools. These patterns show you the most effective way to combine tools
8
+ for common tasks.
9
+
10
+ ## Pattern 1: Find and Search with Glob + Grep
11
+
12
+ The most powerful pattern for code analysis is combining Glob and Grep.
13
+
14
+ **Find files of a type, then search their contents:**
15
+
16
+ ```ruby
17
+ # Step 1: Find relevant files
18
+ result = Glob.new.call(pattern: "**/*.rb", path: "/path/to/project")
19
+
20
+ # Step 2: Search for a pattern in those files
21
+ Grep.new.call(pattern: "class.*API", path: "/path/to/project", include: "*.rb")
22
+ ```
23
+
24
+ **Search first to narrow, then read specific files:**
25
+
26
+ ```ruby
27
+ # Step 1: Find where a method is used
28
+ Grep.new.call(pattern: "def process_payment", path: "/project")
29
+
30
+ # Step 2: Read the specific files found
31
+ Read.new.call(path: "/project/app/services/payment_processor.rb")
32
+ ```
33
+
34
+ ## Pattern 2: Read Files with Offset/Limit
35
+
36
+ When reading large files, use offset and limit to navigate:
37
+
38
+ ```ruby
39
+ # Read the first 50 lines
40
+ Read.new.call(path: "large_file.rb", limit: 50)
41
+
42
+ # Then read from where you left off
43
+ Read.new.call(path: "large_file.rb", offset: 50, limit: 50)
44
+ ```
45
+
46
+ This avoids truncation and lets you browse files in chunks.
47
+
48
+ ## Pattern 3: Pipe Shell Commands via Bash
49
+
50
+ For complex queries that aren't a simple grep, use Bash with pipes:
51
+
52
+ ```ruby
53
+ # Count lines of Ruby code
54
+ Bash.new.call(command: "find . -name '*.rb' -exec cat {} \\; | wc -l", workdir: "/project")
55
+
56
+ # Find the top 10 largest files
57
+ Bash.new.call(command: "find . -name '*.rb' -exec wc -l {} \\; | sort -rn | head -10", workdir: "/project")
58
+
59
+ # Search git history
60
+ Bash.new.call(command: "git log --oneline --all --grep='fix' | head -20", workdir: "/project")
61
+ ```
62
+
63
+ ## Pattern 4: Edit with Read + Edit + Read
64
+
65
+ The safe way to make changes is Read → Edit → Read:
66
+
67
+ ```ruby
68
+ # Step 1: Read the file to find exact text
69
+ Read.new.call(path: "app/models/user.rb")
70
+
71
+ # Step 2: Edit with exact match (provide surrounding context for uniqueness)
72
+ Edit.new.call(
73
+ path: "app/models/user.rb",
74
+ old_string: "has_many :posts",
75
+ new_string: "has_many :posts, dependent: :destroy"
76
+ )
77
+
78
+ # Step 3: Verify the change
79
+ Read.new.call(path: "app/models/user.rb", limit: 30)
80
+ ```
81
+
82
+ When a string could appear multiple times:
83
+ ```ruby
84
+ # First verify occurrences
85
+ Grep.new.call(pattern: "old_string", path: "/project")
86
+
87
+ # Then replace all if appropriate
88
+ Edit.new.call(path: "file.rb", old_string: "old", new_string: "new", replace_all: true)
89
+ ```
90
+
91
+ ## Pattern 5: Code Execution for Quick Ruby Checks
92
+
93
+ Use Code for ad-hoc Ruby that doesn't need tools:
94
+
95
+ ```ruby
96
+ # Check Ruby version or gem availability
97
+ Code.new.call(code: "puts Gem::Specification.map(&:name).grep(/devise/).first")
98
+
99
+ # Parse and inspect data structures
100
+ Code.new.call(code: "require 'json'; data = JSON.parse(File.read('data.json')); puts data.keys")
101
+
102
+ # Test a regex before using it in Grep
103
+ Code.new.call(code: "puts /foo.*bar/.match?('foo_baz_bar')")
104
+ ```
105
+
106
+ ## Pattern 6: Write Files with Content
107
+
108
+ Create or overwrite files with Write:
109
+
110
+ ```ruby
111
+ # Create a new file
112
+ Write.new.call(path: "/project/app/services/new_service.rb", content: <<~RUBY)
113
+ class NewService
114
+ def call
115
+ # ...
116
+ end
117
+ end
118
+ RUBY
119
+ ```
120
+
121
+ Write creates parent directories automatically — no need for mkdir -p first.
122
+
123
+ ## Pattern 7: Project Structure Overview
124
+
125
+ Get a quick lay of the land:
126
+
127
+ ```ruby
128
+ # List top-level structure
129
+ Read.new.call(path: "/project")
130
+
131
+ # Count files by type
132
+ Bash.new.call(command: "find . -name '*.rb' | sed 's/.*\\.//' | sort | uniq -c | sort -rn", workdir: "/project")
133
+
134
+ # List all models or controllers
135
+ Glob.new.call(pattern: "app/models/**/*.rb", path: "/project")
136
+ Glob.new.call(pattern: "app/controllers/**/*.rb", path: "/project")
137
+ ```
138
+
139
+ ## Pattern 8: Search with Exclusions
140
+
141
+ To exclude test directories or vendor code:
142
+
143
+ ```ruby
144
+ # Grep already excludes .git, node_modules, vendor, .bundle, tmp, log
145
+ # For additional exclusions, use Bash:
146
+ Bash.new.call(
147
+ command: "grep -rn 'search_term' app/ lib/ --include='*.rb' | grep -v '_spec.rb' | head -50",
148
+ workdir: "/project"
149
+ )
150
+ ```
151
+
152
+ ## Pattern 9: Sequential File Processing
153
+
154
+ When you need to apply the same pattern across multiple files:
155
+
156
+ ```ruby
157
+ # Step 1: Find the files
158
+ result = Glob.new.call(pattern: "app/views/**/*.erb", path: "/project")
159
+
160
+ # Step 2: Search for the pattern you need to understand
161
+ Grep.new.call(pattern: "data-controller", path: "/project", include: "*.erb")
162
+ ```
163
+
164
+ ## Efficiency Guide
165
+
166
+ | Task | Best Tool | Why |
167
+ |------|-----------|-----|
168
+ | Find files by name pattern | Glob | Fast, sorted newest first |
169
+ | Search file contents | Grep | Pattern-aware, excludes cruft |
170
+ | Read small files | Read | Line numbers, truncation-safe |
171
+ | Read large files | Read (offset/limit) | Navigate in chunks |
172
+ | Complex shell queries | Bash | Full shell power |
173
+ | Ad-hoc Ruby checks | Code | Direct Ruby execution |
174
+ | Make changes | Edit | Exact string replacement |
175
+ | Create files | Write | Mkdir-p + write in one |
@@ -1,15 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "tempfile"
5
- require "stringio"
6
- require "tmpdir"
3
+ require "ask-sandbox-providers"
7
4
 
8
5
  module Ask
9
6
  module Tools
10
7
  # Execute shell commands in a sandboxed environment.
11
- # Returns stdout, stderr, exit code, and a timed_out flag.
12
- # Output is truncated to 100KB.
8
+ # Runs via Ask::Sandbox.provider (Local by default, configurable for
9
+ # stronger isolation via Docker, Daytona, or Cloudflare).
13
10
  class Bash < Ask::Tool
14
11
  description "Execute a bash command in a sandboxed environment. " \
15
12
  "Returns stdout, stderr, and exit code. " \
@@ -19,59 +16,26 @@ module Ask
19
16
  param :timeout, type: :integer, desc: "Timeout in seconds", required: false
20
17
  param :workdir, type: :string, desc: "Working directory", required: false
21
18
 
22
- MAX_OUTPUT_SIZE = 102_400
23
-
24
19
  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
- })
20
+ result = Ask::Sandbox.provider.call(
21
+ command,
22
+ timeout: timeout,
23
+ workdir: workdir
24
+ )
25
+
26
+ if result.timed_out
27
+ return Ask::Result.error(
28
+ message: "Command timed out",
29
+ metadata: { stdout: result.stdout, stderr: result.stderr }
30
+ )
74
31
  end
32
+
33
+ Ask::Result.ok(data: {
34
+ stdout: result.stdout,
35
+ stderr: result.stderr,
36
+ exit_code: result.exit_code,
37
+ timed_out: result.timed_out
38
+ })
75
39
  end
76
40
  end
77
41
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "tmpdir"
3
+ require "ask-sandbox-providers"
5
4
 
6
5
  module Ask
7
6
  module Tools
8
7
  # Write and execute Ruby code in a subprocess.
9
- # Runs in a temp directory via ruby -e.
8
+ # Runs via Ask::Sandbox.provider (Local by default, configurable for
9
+ # stronger isolation via Docker, Daytona, or Cloudflare).
10
10
  class Code < Ask::Tool
11
11
  description "Write and execute Ruby code in a subprocess. " \
12
12
  "Returns stdout, stderr, and exit code. " \
@@ -14,45 +14,24 @@ module Ask
14
14
 
15
15
  param :code, type: :string, desc: "Ruby source code to execute", required: true
16
16
 
17
- MAX_OUTPUT_SIZE = 102_400
18
-
19
17
  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
- })
18
+ result = Ask::Sandbox.provider.call(
19
+ ["ruby", "-e", code],
20
+ timeout: 30
21
+ )
22
+
23
+ if result.timed_out
24
+ return Ask::Result.error(
25
+ message: "Code execution timed out",
26
+ metadata: { stdout: result.stdout, stderr: result.stderr }
27
+ )
55
28
  end
29
+
30
+ Ask::Result.ok(data: {
31
+ stdout: result.stdout,
32
+ stderr: result.stderr,
33
+ exit_code: result.exit_code
34
+ })
56
35
  end
57
36
  end
58
37
  end
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module Tools
5
5
  module Shell
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.1"
7
7
  end
8
8
  end
9
9
  end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "shell/version"
4
+ require_relative "shell/bash"
5
+ require_relative "shell/read"
6
+ require_relative "shell/write"
7
+ require_relative "shell/edit"
8
+ require_relative "shell/glob"
9
+ require_relative "shell/grep"
10
+ require_relative "shell/code"
11
+
3
12
  module Ask
4
13
  module Tools
5
- # Collection point for shell tools.
6
- #
7
- # Ask::Tools::Shell.all # => [Bash, Read, Write, ...] instances
8
- #
9
14
  module Shell
10
15
  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
16
  end
19
17
  end
20
18
  end
@@ -1,13 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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"
4
+ require "ask/tools/shell"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-tools-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ask-sandbox-providers
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: minitest
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +89,7 @@ files:
75
89
  - LICENSE
76
90
  - README.md
77
91
  - lib/ask-tools-shell.rb
92
+ - lib/ask/skills/shell.patterns/SKILL.md
78
93
  - lib/ask/tools/shell.rb
79
94
  - lib/ask/tools/shell/bash.rb
80
95
  - lib/ask/tools/shell/code.rb