lux-hammer 0.3.25 → 0.3.27

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: cbcb0355f0ce770acb1ace879a1567ee1193de1da5ac6e6c32e36faa8f2a48fe
4
+ data.tar.gz: 8299d3bf2a47844e0e5c01ba73225d43be8aef718ceee3cf9603d0ec85e3e582
5
5
  SHA512:
6
- metadata.gz: 8c9e26f85ded5283505130548bcd1b64718d1a405013149d086881e166c59239cb1a4db3fbefcb2a990ed2a06b82d90ae54769e6650be6d1d6032a9fe48328d5
7
- data.tar.gz: 700f84954436b3e606d2b5234ff682f19c27d651083037e0faaa0f9ca796b1df604704d6908d58f7fa6a133dc9fa314df71084a805c6219b1d46d963557fe22b
6
+ metadata.gz: 6f5a7f793308a5f55e1bdb99ab85ae7aa8b271c92a073f33e1e7130f757395a97071da89a0933958cd89279a7a2004372a71746f1d83c945747ea1cb785f9f75
7
+ data.tar.gz: b5ee6d077d5b14fd5c448a28ea53e15da59198c067a568bee97193b5352cfea78dd073dff9054af58501453a72cbdae5b0d6c6af5e3e578004b102434a98b29d
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.25
1
+ 0.3.27
data/AGENTS.md CHANGED
@@ -22,8 +22,9 @@ usable as a library (`require 'lux-hammer'`, subclass `Hammer`, call
22
22
  monkey-patches.
23
23
  * **Zero runtime dependencies**. The gem must work with stdlib only. New
24
24
  dependencies require explicit user approval.
25
- * **Ruby >= 2.7**. Do not use language features introduced after 2.7
26
- without flagging.
25
+ * **Ruby >= 2.7** for `lib/` and `bin/`. Do not use language features
26
+ introduced after 2.7 there without flagging. Bundled recipes may sit
27
+ higher - the `llm` recipe needs >= 3.1 (endless methods, shorthand hash).
27
28
  * **`desc` is single-arg, multi-line allowed**. The argument is one
28
29
  string; it may contain newlines. The first line is the brief shown in
29
30
  command listings, the full string renders (indented) in per-command
@@ -300,7 +301,10 @@ Task contracts:
300
301
  `--path NAME`, `--edit NAME`, `--run NAME [ARGS]` (use `--` to
301
302
  forward flags through). Bare invocation lists.
302
303
  * `h:init` - writes `Hammer::STARTER_HAMMERFILE` to `./Hammerfile`;
303
- refuses if one exists.
304
+ refuses if one exists. `--script` writes `Hammer::STARTER_SCRIPT`
305
+ (shebang + one `hello` task) to stdout instead, or to a positional
306
+ TARGET with mode 0755. Both templates are single-quoted heredocs in
307
+ `lib/lux-hammer.rb` - the one source of truth, also rendered in help.
304
308
  * `h:json` - `puts JSON` of `root.export_spec` (tasks grouped exactly
305
309
  like the bare listing via `section_for`, root group keyed `__root`).
306
310
  `--all` keeps the `h:` tree, `--compact` minifies.
data/README.md CHANGED
@@ -634,6 +634,9 @@ Under the `h:` namespace:
634
634
  * `h:version` - print the lux-hammer version.
635
635
  * `h:recipes` - list / install / show / edit recipes.
636
636
  * `h:init` - write a starter Hammerfile in cwd (refuses if one exists).
637
+ `--script` prints a minimal standalone CLI to stdout instead (shebang
638
+ plus one `hello` task) - pipe it to a file and `chmod +x`, or name a
639
+ target to write and chmod in one step.
637
640
  * `h:json` - dump the CLI definition as JSON (tasks grouped like the
638
641
  bare listing, with desc/options/examples/aliases/needs/cron); `--all`
639
642
  includes the `h:` tasks, `--compact` minifies. Output is plain stdout
@@ -1135,7 +1138,16 @@ end
1135
1138
  ### `#!/usr/bin/env hammer` (single-file scripts)
1136
1139
 
1137
1140
  For a one-off CLI that lives as a single executable file, point the
1138
- shebang straight at `hammer` - no `require`, no boilerplate:
1141
+ shebang straight at `hammer` - no `require`, no boilerplate. Start from
1142
+ the bundled template:
1143
+
1144
+ ```sh
1145
+ $ hammer h:init --script greet # or: h:init --script > greet
1146
+ $ ./greet hello
1147
+ Hello from Hammer
1148
+ ```
1149
+
1150
+ Then fill it in - the body is plain Hammerfile DSL:
1139
1151
 
1140
1152
  ```ruby
1141
1153
  #!/usr/bin/env hammer
@@ -108,8 +108,24 @@ class Hammer
108
108
  def register_init(klass)
109
109
  klass.class_eval do
110
110
  task :init do
111
- desc 'Write a starter Hammerfile in the current directory'
112
- proc { Hammer::Builtins.write_starter_hammerfile }
111
+ desc <<~TXT
112
+ Write a starter Hammerfile in the current directory.
113
+
114
+ --script prints a minimal standalone CLI (shebang + one `hello`
115
+ task) to stdout instead - pipe it to a file, chmod +x, run it.
116
+ Name a TARGET to write and chmod in one step.
117
+ TXT
118
+ example 'h:init'
119
+ example 'h:init --script > mytool && chmod +x mytool'
120
+ example 'h:init --script mytool'
121
+ opt :script, type: :boolean, alias: :s, desc: 'print a standalone executable CLI (TARGET writes + chmod +x)'
122
+ proc do |opts|
123
+ if opts[:script]
124
+ Hammer::Builtins.write_starter_script(opts[:args].first)
125
+ else
126
+ Hammer::Builtins.write_starter_hammerfile
127
+ end
128
+ end
113
129
  end
114
130
  end
115
131
  end
@@ -237,6 +253,20 @@ class Hammer
237
253
  Shell.say "created #{target}", :green
238
254
  end
239
255
 
256
+ # `h:init --script`. No target -> stdout, so `> tool` works; with a
257
+ # target it lands executable. Never clobbers, same as the Hammerfile.
258
+ def write_starter_script(target = nil)
259
+ return puts(Hammer::STARTER_SCRIPT) unless target
260
+
261
+ if File.exist?(target)
262
+ Shell.print_error "#{target} already exists"
263
+ exit 1
264
+ end
265
+ File.write(target, Hammer::STARTER_SCRIPT)
266
+ File.chmod(0o755, target)
267
+ Shell.say "created #{target} (chmod +x)", :green
268
+ end
269
+
240
270
  # Implementations of the `recipes` task's action flags, plus the
241
271
  # no-flag listing view. Separate module so the task definition above
242
272
  # stays small.
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?
data/lib/lux-hammer.rb CHANGED
@@ -1118,6 +1118,20 @@ class Hammer
1118
1118
  end
1119
1119
  RUBY
1120
1120
 
1121
+ # Minimal standalone CLI, dumped by `hammer h:init --script`. The
1122
+ # shebang is what `shebang_script` sniffs for, so `chmod +x` is all
1123
+ # that stands between this and a working binary.
1124
+ STARTER_SCRIPT ||= <<~'RUBY'
1125
+ #!/usr/bin/env hammer
1126
+
1127
+ desc 'My standalone CLI'
1128
+
1129
+ task :hello do
1130
+ desc 'Print a greeting'
1131
+ proc { say.green 'Hello from Hammer' }
1132
+ end
1133
+ RUBY
1134
+
1121
1135
  # Default install dir used by install.sh and `hammer h:update`.
1122
1136
  SELF_UPDATE_DIR ||= File.expand_path('~/.local/share/lux-hammer')
1123
1137
  SELF_UPDATE_REPO ||= 'https://github.com/dux/hammer.git'
@@ -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.27
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-09-08 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"