lux-hammer 0.3.25 → 0.3.26

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: 273ffb060fd16cb52423002e5e8e041481a37b0487395d966a8f9ba90cfa53fa
4
- data.tar.gz: 9dc14e358bfafc0050379d671711ca28d6d6eb3aa1935f7440c93ea897267431
3
+ metadata.gz: acc942b0818d0cc60a8cbe9a55fbf092665d6efcb46df209b2a8e9721aa1e190
4
+ data.tar.gz: 94baed7a0025611360d0315d084fd7432a1eb68d84302bfeaf0960ee0326aee7
5
5
  SHA512:
6
- metadata.gz: 8c9e26f85ded5283505130548bcd1b64718d1a405013149d086881e166c59239cb1a4db3fbefcb2a990ed2a06b82d90ae54769e6650be6d1d6032a9fe48328d5
7
- data.tar.gz: 700f84954436b3e606d2b5234ff682f19c27d651083037e0faaa0f9ca796b1df604704d6908d58f7fa6a133dc9fa314df71084a805c6219b1d46d963557fe22b
6
+ metadata.gz: 486bb304e25a56faada26757651998d75e589fabca7402f499dc9cdd3228a90b30cc3fc7cda765d2cd34fc90eecc906208681c031e38628e0087f59eb5250af7
7
+ data.tar.gz: 87115686cf4bbdd6a47ed9a25e048cf811fe1c8071966e5ee3143b7fcafc286ff1480d00c5ba494e407fa9032b8e1b6dda43e8623c011ae9ffdbdf70e8a33612
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.25
1
+ 0.3.26
data/lib/hammer/parser.rb CHANGED
@@ -50,6 +50,17 @@ class Hammer
50
50
  next
51
51
  end
52
52
 
53
+ # Bundled boolean short flags: `-cf` is `-c -f`. Only when every letter
54
+ # is a boolean switch, so `-pVALUE` below keeps its meaning.
55
+ if token =~ /\A-[^-]{2,}\z/
56
+ bundle = token[1..-1].chars.map { |ch| @by_switch["-#{ch}"] }
57
+ if bundle.all? { |o| o&.boolean? }
58
+ bundle.each { |o| values[o.name] = true }
59
+ i += 1
60
+ next
61
+ end
62
+ end
63
+
53
64
  # Glued short flag with value: `-pVALUE` (non-boolean short opts only).
54
65
  if token.start_with?('-') && !token.start_with?('--') && token.length > 2
55
66
  if (opt = @by_switch[token[0, 2]]) && !opt.boolean?
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pathname'
5
+
6
+ # Builds the command `llm wrap:run` hands to LlmWrap: picks the agent CLI by
7
+ # prefix and maps the generic -c / -f / -a switches to what that CLI takes.
8
+ module LlmLaunch
9
+ Launch = Struct.new(:argv, :env, :files)
10
+
11
+ # :agents gets the combined AGENTS.md text and the file paths, and answers
12
+ # with the argv to add and/or env to set - opencode has no flag for
13
+ # instructions, only inline config, and that one takes paths.
14
+ TOOLS ||= {
15
+ 'claude' => { continue: %w[-c], full: %w[--dangerously-skip-permissions],
16
+ agents: ->(text, _files) { { args: ['--append-system-prompt', text] } } },
17
+ 'codex' => { continue: %w[resume --last], full: %w[--dangerously-bypass-approvals-and-sandbox],
18
+ agents: ->(text, _files) { { args: ['-c', "developer_instructions=#{text}"] } } },
19
+ 'grok' => { continue: %w[-c], full: %w[--always-approve],
20
+ agents: ->(text, _files) { { args: ['--rules', text] } } },
21
+ 'opencode' => { continue: %w[-c], full: %w[--auto],
22
+ agents: ->(_text, files) { { env: { 'OPENCODE_CONFIG_CONTENT' => { instructions: files }.to_json } } } },
23
+ }.freeze
24
+
25
+ # Read AGENTS.md from the git root down on their own; -a only adds the
26
+ # files above it for these. Claude reads CLAUDE.md, so it gets the chain.
27
+ NATIVE_AGENTS ||= %w[codex grok opencode].freeze
28
+
29
+ # First tool whose name starts with the prefix, in TOOLS order: `c` is
30
+ # claude, `co` codex. No prefix means claude.
31
+ def self.tool(prefix)
32
+ return 'claude' if prefix.nil? || prefix.empty?
33
+ TOOLS.keys.find { |name| name.start_with?(prefix) }
34
+ end
35
+
36
+ def self.build(tool, extra = [], continue: false, full: false, agents: false, cwd: Dir.pwd, home: Dir.home)
37
+ spec = TOOLS.fetch(tool)
38
+ argv = [tool]
39
+ argv.concat(spec[:continue]) if continue
40
+ argv.concat(spec[:full]) if full
41
+
42
+ files = agents ? agents_files(tool, cwd: cwd, home: home) : []
43
+ env = {}
44
+ if files.any?
45
+ added = spec[:agents].call(agents_text(files), files.map(&:to_s))
46
+ argv.concat(added[:args] || [])
47
+ env.update(added[:env] || {})
48
+ end
49
+
50
+ argv.concat(extra)
51
+ Launch.new(argv, env, files)
52
+ end
53
+
54
+ # AGENTS.md from home down to cwd, outermost first. For NATIVE_AGENTS tools
55
+ # the walk stops above the git root - or above cwd outside git, since that is
56
+ # what they take as the project then.
57
+ def self.agents_files(tool, cwd: Dir.pwd, home: Dir.home)
58
+ stop = nil
59
+ if NATIVE_AGENTS.include?(tool)
60
+ stop = IO.popen(['git', '-C', cwd, 'rev-parse', '--show-toplevel'], err: File::NULL, &:read).to_s.strip
61
+ stop = cwd if stop.empty?
62
+ end
63
+
64
+ Pathname(cwd).ascend
65
+ .select { |dir| dir.to_s.start_with?(home) }
66
+ .reject { |dir| stop && dir.to_s.start_with?(stop) }
67
+ .reverse
68
+ .map { |dir| dir + 'AGENTS.md' }
69
+ .select(&:file?)
70
+ end
71
+
72
+ def self.agents_text(files)
73
+ body = files.map { |f| "## #{f}\n\n#{f.read.strip}\n" }.join("\n")
74
+ "Project instructions (AGENTS.md), broad to specific:\n\n#{body}"
75
+ end
76
+ end
data/recipes/llm.rb CHANGED
@@ -13,6 +13,7 @@ desc <<~TXT
13
13
  Commands:
14
14
  usage subscription limits and per-model token usage
15
15
  wrap run a command with your last two prompts pinned to the screen
16
+ wrap:run start claude / codex / grok / opencode in wrap, same -c -f -a switches for all
16
17
  TXT
17
18
 
18
19
  require 'fileutils'
@@ -185,6 +186,58 @@ task :wrap do
185
186
  end
186
187
  end
187
188
 
189
+ namespace :wrap do
190
+ task :run do
191
+ desc <<~D
192
+ Start an agent CLI inside `llm wrap`, with the same switches whichever one it is.
193
+
194
+ TOOL is a prefix of claude, codex, grok or opencode - first match wins, so `c` is
195
+ claude and `co` is codex. No tool means claude. The switches map to what each
196
+ CLI actually takes:
197
+
198
+ -c continue the last session here claude/grok/opencode -c, codex resume --last
199
+ -f full permissions, no prompts --dangerously-skip-permissions, --dangerously-bypass-
200
+ approvals-and-sandbox, --always-approve, --auto
201
+ -a AGENTS.md from ~ down to cwd claude --append-system-prompt, codex -c
202
+ developer_instructions, grok --rules, opencode
203
+ inline config
204
+
205
+ codex, grok and opencode read AGENTS.md from the git root down by themselves, so
206
+ -a only adds the files above the repo for them; claude gets the whole chain.
207
+ Anything after `--` is handed to the tool untouched. `~/bin/ai` is this command.
208
+ D
209
+ example 'wrap:run'
210
+ example 'wrap:run g -cf'
211
+ example 'wrap:run cod -ca'
212
+ example 'wrap:run c -- --model opus'
213
+
214
+ opt :continue, type: :boolean, desc: 'continue the last session in this folder'
215
+ opt :full, type: :boolean, desc: 'full permissions, no approval prompts'
216
+ opt :agents, type: :boolean, desc: 'inject AGENTS.md found from ~ down to cwd'
217
+
218
+ proc do |opts|
219
+ require File.join(_llm_root, 'lib/llm/wrap')
220
+ require File.join(_llm_root, 'lib/llm/launch')
221
+
222
+ args = Array(opts[:args])
223
+ prefix = args.first unless args.first.nil? || args.first.start_with?('-')
224
+ tool = LlmLaunch.tool(prefix) ||
225
+ error("unknown tool #{prefix.inspect} - one of #{LlmLaunch::TOOLS.keys.join(', ')}")
226
+ extra = prefix ? args.drop(1) : args
227
+
228
+ launch = LlmLaunch.build(tool, extra, continue: opts[:continue], full: opts[:full], agents: opts[:agents])
229
+ ENV.update(launch.env)
230
+ say.gray "agents: #{launch.files.map { |f| f.to_s.sub(Dir.home, '~') }.join(', ')}" if launch.files.any?
231
+
232
+ flags = %i[continue full agents].select { |k| opts[k] }.map { |k| "--#{k}" }
233
+ origin = ['llm', 'wrap:run', tool, *flags, '--', *extra]
234
+ # Piped stdout is block-buffered and exec drops the buffer with the banner in it.
235
+ $stdout.flush
236
+ exit LlmWrap.run(launch.argv, origin: origin)
237
+ end
238
+ end
239
+ end
240
+
188
241
  namespace :memory do
189
242
  # Helpers are defined inside the namespace block (class_eval'd on the
190
243
  # namespace's anonymous Hammer subclass) so the task procs reach them.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lux-hammer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.25
4
+ version: 0.3.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dino Reic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -55,6 +55,7 @@ files:
55
55
  - "./lib/lux-hammer.rb"
56
56
  - "./recipes/deploy.rb"
57
57
  - "./recipes/git-helper.rb"
58
+ - "./recipes/lib/llm/launch.rb"
58
59
  - "./recipes/lib/llm/plan.rb"
59
60
  - "./recipes/lib/llm/usage.rb"
60
61
  - "./recipes/lib/llm/wrap.rb"