lux-hammer 0.3.23 → 0.3.24

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: 74dd44bca6cf689dcfe0396d90e05e51b0605821314fb6d5b742502662f4e8e8
4
- data.tar.gz: a84eaa4fcce83f951905bab3419d18adfcce409cd0f9d2f23db910569443a2ce
3
+ metadata.gz: 68c2cd8d90138ab3eaf6a68a23aeabc273b31d26122224ba3966d235a9841657
4
+ data.tar.gz: ae13bf1ca96c396b51a1db3801ce4aa4179ad67f5bb8e4b04467715b9744164e
5
5
  SHA512:
6
- metadata.gz: 044a31c525aba212012888f39d938fa37dc2e69e59097fa10f1dda53723b0a267dae2dad0e77887c19ad801a64fcb47e03b8aa95d916b04474d5239628a9b8f0
7
- data.tar.gz: 3ec9940429c06e4bfb00669184fe3d4f78fff0ed693f857fcd37c682bf89b70a214f3a7e92e24826a1ab5889c62de26f05e0f9b78fe22c1c65b12227d4a18681
6
+ metadata.gz: 222754053a8ca203a8f0344fd0007174e1a333f92e69935fd908567e7e6a8a1d1c8675d5bd8a443a905fd9140ba54ed4afc2fdbd7abe817c8cc90d3643e97fa1
7
+ data.tar.gz: 345ebe94f0d5eb3383fb4a0df98a1cc95d611fa330d2174c2ed6b3b25de724ee0acc51100682cd93cce813c446298cd0e19d004998e3fe95e8f5b01434368faa
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.23
1
+ 0.3.24
data/README.md CHANGED
@@ -506,9 +506,11 @@ end
506
506
 
507
507
  ## Stdin, JSON input, and global options
508
508
 
509
- Every command receives **`opts[:stdin]`** automatically when stdin is a
510
- pipe (non-TTY) and non-empty; otherwise it is `nil`. Interactive TTYs are
511
- never consumed.
509
+ Every command can read **`opts[:stdin]`**: piped stdin (non-TTY, non-empty)
510
+ as a string, otherwise `nil`. It is read on the first lookup and kept, so a
511
+ command that never looks never waits on a pipe. Interactive TTYs are never
512
+ consumed, and a socket with nothing pending (agent harnesses hand one to
513
+ every command) reads as `nil` rather than blocking.
512
514
 
513
515
  ```ruby
514
516
  task :count do
data/lib/hammer/input.rb CHANGED
@@ -3,18 +3,23 @@
3
3
  require 'json'
4
4
 
5
5
  class Hammer
6
- # Stdin + JSON helpers for opts. Hammer always attaches piped stdin as
7
- # `opts[:stdin]`; recipes opt into JSON body handling via
8
- # `Hammer::Input.prepare_json!` (typically from a `before` hook) and/or
9
- # `opt :json, type: :json` / `global_opt :json, type: :json`.
6
+ # Stdin + JSON helpers for opts. Hammer exposes piped stdin as
7
+ # `opts[:stdin]`, read lazily on first access; recipes opt into JSON body
8
+ # handling via `Hammer::Input.prepare_json!` (typically from a `before`
9
+ # hook) and/or `opt :json, type: :json` / `global_opt :json, type: :json`.
10
10
  module Input
11
11
  module_function
12
12
 
13
13
  # Read piped stdin once. Returns nil when stdin is a TTY or empty.
14
- # Does not consume an interactive TTY.
14
+ # Does not consume an interactive TTY, and does not block on a socket
15
+ # that has nothing pending: agent harnesses (Claude Code's Bash tool)
16
+ # hand every command a unix socket as stdin that never reaches EOF, so
17
+ # a plain read would hang there for good. A shell pipe or a redirected
18
+ # file is a FIFO or a regular file and is still read in full.
15
19
  def read_stdin
16
20
  return nil if $stdin.closed?
17
21
  return nil if $stdin.tty?
22
+ return nil if idle_socket?($stdin)
18
23
 
19
24
  data = $stdin.read
20
25
  return nil if data.nil?
@@ -25,11 +30,26 @@ class Hammer
25
30
  nil
26
31
  end
27
32
 
28
- # Idempotent: sets opts[:stdin] if not already present.
33
+ def idle_socket?(io)
34
+ return false unless io.respond_to?(:stat) && io.stat.socket?
35
+
36
+ IO.select([io], nil, nil, 0).nil?
37
+ rescue StandardError
38
+ false
39
+ end
40
+
41
+ # Reads stdin into the hash on the first `opts[:stdin]` lookup and keeps
42
+ # the result, so a command that never asks for stdin never waits on it.
43
+ LAZY_STDIN = lambda do |hash, key|
44
+ key == :stdin ? (hash[:stdin] = Input.read_stdin) : nil
45
+ end
46
+
47
+ # Idempotent: leaves an explicit opts[:stdin] alone, otherwise installs
48
+ # the lazy reader. Nothing is read until a recipe looks at opts[:stdin].
29
49
  def attach_stdin!(opts)
30
- return opts[:stdin] if opts.key?(:stdin)
50
+ return if opts.key?(:stdin) || opts.default_proc.equal?(LAZY_STDIN)
31
51
 
32
- opts[:stdin] = read_stdin
52
+ opts.default_proc = LAZY_STDIN
33
53
  end
34
54
 
35
55
  # Parse a JSON source string into a Hash/Array with symbol keys.
data/lib/lux-hammer.rb CHANGED
@@ -690,8 +690,9 @@ class Hammer
690
690
  options = effective_options(cmd)
691
691
  positional, opts = Parser.new(options).parse(argv)
692
692
  opts[:args] = positional
693
- # Always attach piped stdin (nil when TTY / empty). Recipes that want
694
- # JSON body handling call Hammer::Input.prepare_json! in a before hook.
693
+ # opts[:stdin] reads piped stdin on first access (nil when TTY / empty).
694
+ # Recipes that want JSON body handling call Hammer::Input.prepare_json!
695
+ # in a before hook.
695
696
  Hammer::Input.attach_stdin!(opts)
696
697
  print_run_banner(cmd, full || cmd.name, positional, opts, options: options) unless quiet || ENV['HAMMER_QUIET']
697
698
  instance = new
data/recipes/llm.rb CHANGED
@@ -337,7 +337,9 @@ TODO_GUIDE ||= <<~TXT
337
337
  Per-project task queue in ./LLM_TODO.local.md - workable by humans and any agent.
338
338
 
339
339
  Human usage:
340
- llm todo:add "fix login redirect" queue a task (pipe stdin for bulk / multi-line)
340
+ llm todo:add "fix login redirect" queue a task; pipe stdin for bulk:
341
+ `* ` bullets delimit tasks (multi-line ok),
342
+ no bullets at all = one task per line
341
343
  llm todo:list tasks by id, counts, format warnings
342
344
  llm todo:pop + llm todo:done take / finish a single task by hand
343
345
 
@@ -491,9 +493,10 @@ namespace :todo do
491
493
  desc <<~DESC
492
494
  Add task(s) to ./#{TODO_FILE} (created on first add).
493
495
 
494
- Text comes from the argument (one task), or from stdin - there a `*` at
495
- the start of a line begins a new task and following lines belong to it,
496
- so multi-line tasks and whole lists can be piped in at once.
496
+ Text comes from the argument (one task), or from stdin. Piped input
497
+ that has `*` / `-` bullets is a list: a bullet begins a task and the
498
+ lines under it belong to it, so multi-line tasks and whole lists can be
499
+ piped in at once. Input with no bullets at all is one task per line.
497
500
  DESC
498
501
  example 'todo add "migrate the user model to Sequel"'
499
502
  example 'cat tasks.md | llm todo add'
@@ -503,17 +506,19 @@ namespace :todo do
503
506
  # unquoted multi-word input lands in :text + :args - join it back
504
507
  arg = [opts[:text], *opts[:args]].compact.join(' ').strip
505
508
  if arg.empty?
509
+ lines = opts[:stdin].to_s.split("\n")
510
+ bulleted = lines.any? { |l| l =~ /^[-*]\s+/ }
506
511
  new_tasks = []
507
- opts[:stdin].to_s.split("\n").each do |line|
512
+ lines.each do |line|
508
513
  if line =~ /^[-*]\s+(.*)$/
509
514
  new_tasks << [$1.strip]
510
- elsif new_tasks.any?
511
- new_tasks.last << line.rstrip
515
+ elsif bulleted
516
+ new_tasks.last << line.rstrip if new_tasks.any?
512
517
  elsif line.strip != ''
513
518
  new_tasks << [line.rstrip]
514
519
  end
515
520
  end
516
- new_tasks = new_tasks.map { |lines| lines.join("\n").rstrip }.reject(&:empty?)
521
+ new_tasks = new_tasks.map { |ls| ls.join("\n").rstrip }.reject(&:empty?)
517
522
  else
518
523
  new_tasks = [arg]
519
524
  end
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.23
4
+ version: 0.3.24
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-19 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest