lux-hammer 0.3.17 → 0.3.21

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.
data/recipes/llm.rb CHANGED
@@ -10,7 +10,8 @@ desc <<~TXT
10
10
  prompt token-prefix prompt expander (UserPromptSubmit hook + CLI)
11
11
 
12
12
  Commands:
13
- usage Claude, Codex, and Grok subscription usage (session + week table)
13
+ usage subscription limits and per-model token usage
14
+ wrap run a command with your last two prompts pinned to the screen
14
15
  TXT
15
16
 
16
17
  require 'fileutils'
@@ -18,50 +19,127 @@ require 'json'
18
19
  require 'pathname'
19
20
  require 'set'
20
21
 
21
- _llm_root = File.dirname(Hammer::Recipe.path('llm') || File.join(Hammer::Recipe::GEM_DIR, 'llm.rb'))
22
+ # Resolve next to this recipe file (realpath so PATH symlinks to a dev
23
+ # checkout still load matching lib/, not an older installed gem copy).
24
+ _llm_root = File.dirname(File.realpath(__FILE__))
22
25
  require File.join(_llm_root, 'lib/llm/usage')
23
26
 
24
- STORE ||= ENV['CLAUDE_MEMORY_STORE'] || File.expand_path('~/dev/skills/memory')
27
+ STORE ||= ENV['CLAUDE_MEMORY_STORE'] || File.expand_path('~/dev/ai/memory')
25
28
  VALID_TYPES ||= %w[user feedback project reference].freeze
26
29
 
27
30
  FileUtils.mkdir_p(STORE)
28
31
 
29
32
  task :usage do
30
33
  desc <<~D
31
- Show subscription usage for Claude, Codex, and Grok in one table.
34
+ Show subscription limits and per-model token usage for Claude, Codex, and Grok.
32
35
 
33
36
  Default view lists session and weekly windows side by side.
34
- Reads Codex via `codex app-server`, Grok from `~/.grok/logs/unified.jsonl`.
35
- Claude has no local snapshot. Pass `month` for Claude extra-usage when enabled.
37
+ Local Claude, Codex, and Grok sessions provide calendar day, week, and month token totals.
38
+ Reads Codex via `codex app-server`; Grok billing + tokens from `~/.grok/logs/unified.jsonl`
39
+ (inference_done events). Claude 5h/week windows come from the statusline snapshot at
40
+ `~/.cache/llm/claude-limits.json`, falling back to the OAuth usage API. `month` shows
41
+ Claude extra-usage credits (API only, and only when you've enabled them).
36
42
  D
37
43
  example 'llm usage'
38
44
  example 'llm usage month'
39
45
  example 'llm usage --json'
40
46
  example 'llm usage --provider grok'
41
47
 
48
+ # :period is declared first on purpose — the parser fills un-set scalar opts
49
+ # from positionals in declaration order, so `llm usage month` must reach
50
+ # :period before :provider can claim it.
51
+ opt :period, desc: 'window to show (week, month)', placeholder: 'week|month'
42
52
  opt :json, type: :boolean, default: false, desc: 'emit JSON'
43
53
  opt :provider, desc: 'single provider (claude, codex, grok)'
44
54
 
45
55
  proc do |opts|
46
- period = opts[:args].first
56
+ period = opts[:period]
47
57
  providers = opts[:provider] ? [opts[:provider]] : nil
48
58
  rows, notes, = LlmUsage.collect_rows(period: period, providers: providers)
59
+ token_rows = LlmUsage.collect_token_rows(providers: providers)
49
60
 
50
- if rows.empty?
61
+ if rows.empty? && token_rows.empty?
51
62
  notes.each { |note| say note, :yellow }
52
63
  error 'no usage data (sign in to Claude, Codex, or Grok first)' if notes.empty?
53
64
  error 'no usage data available'
54
65
  end
55
66
 
56
67
  if opts[:json]
57
- puts JSON.generate(LlmUsage.rows_to_json(rows, period: period, notes: notes))
68
+ puts JSON.generate(
69
+ LlmUsage.rows_to_json(rows, period: period, notes: notes, token_rows: token_rows)
70
+ )
58
71
  else
59
- say LlmUsage.render_table(rows, period: period)
72
+ tables = []
73
+ tables << LlmUsage.render_table(rows, period: period) unless rows.empty?
74
+ tables << LlmUsage.render_token_table(token_rows) unless token_rows.empty?
75
+ say tables.join("\n\n")
60
76
  notes.each { |note| say note, :gray }
61
77
  end
62
78
  end
63
79
  end
64
80
 
81
+ task :wrap do
82
+ desc <<~D
83
+ Run a command with the last few prompts you typed pinned to the bottom of the screen.
84
+
85
+ The command runs in a PTY that is shorter than the real terminal by the height of
86
+ the bar, so those rows belong to the bar and nothing the program draws can cover
87
+ them - no hooks, no tmux, no terminal-specific plumbing. Works with claude, codex,
88
+ a REPL, a plain shell.
89
+
90
+ The bar is a "prompt history" rule with the prompts below it, oldest first, so the
91
+ newest one sits on the very last line. It is --lines + 1 rows tall.
92
+
93
+ Each prompt gets one row: a multi-line prompt is flattened onto it with line breaks
94
+ shown as a backslash, and the whole thing is clipped to the terminal width.
95
+
96
+ A prompt is whatever you type between Enters. Shift/Option+Enter and pasted newlines
97
+ keep appending to the current one. This is keystroke sniffing, so text the program
98
+ inserts for you (history recall, autocomplete, menu picks) will not show up, and
99
+ nothing is captured while the program has echo off in canonical mode (sudo, ssh).
100
+
101
+ Keys are decoded from plain bytes, the kitty keyboard protocol and xterm
102
+ modifyOtherKeys, since a program can ask the terminal for any of the three (Claude
103
+ Code turns on the last two). Mouse reports, focus events, arrows and Ctrl-<key> are
104
+ not text and are dropped - including Ctrl-V, which pastes an image in Claude Code.
105
+
106
+ LLM_WRAP_DEBUG=<file> logs what arrived and how it was read, for when a terminal
107
+ reports keys in some way this does not know about. It records every keystroke.
108
+
109
+ Put `--` before the wrapped command when it has flags of its own, otherwise they are
110
+ parsed as flags to `llm wrap`.
111
+ D
112
+ example 'llm wrap claude'
113
+ example 'llm wrap -- claude --resume'
114
+ example 'llm wrap --lines 5 -- bash -l'
115
+
116
+ # `positional: false` is load-bearing: the parser fills un-set scalar opts
117
+ # from positionals in declaration order, so without it `llm wrap claude`
118
+ # would hand "claude" to --lines and die on the integer cast.
119
+ opt :lines, type: :integer, default: 3, positional: false,
120
+ desc: 'how many recent prompts to pin', placeholder: 'N'
121
+
122
+ proc do |opts|
123
+ cmd = Array(opts[:args])
124
+ error 'usage: llm wrap [--lines N] [--] <command> [args...]' if cmd.empty?
125
+
126
+ # Required lazily - llm.rb also runs as a per-prompt hook (`llm prompt
127
+ # hook`), which should not pay to load pty/io-console. `_llm_root` is a
128
+ # local from the file scope; the block closes over it (instance_exec
129
+ # rebinds self, not locals).
130
+ require File.join(_llm_root, 'lib/llm/wrap')
131
+
132
+ # The wrapper renames itself after the program it runs, so that a pane
133
+ # watcher can still see which agent is in there - which leaves nothing
134
+ # behind that says how the pane was started. This is that: the invocation
135
+ # rebuilt from what the parser gave us, since by then the real command line
136
+ # is a resolved interpreter chain nobody typed. `--` keeps a wrapped
137
+ # command's own flags out of our parser when it is read back.
138
+ origin = ['llm', 'wrap', '--lines', opts[:lines].to_s, '--', *cmd]
139
+ exit LlmWrap.run(cmd, keep: opts[:lines], origin:)
140
+ end
141
+ end
142
+
65
143
  namespace :memory do
66
144
  # Helpers are defined inside the namespace block (class_eval'd on the
67
145
  # namespace's anonymous Hammer subclass) so the task procs reach them.
@@ -139,15 +217,21 @@ namespace :memory do
139
217
  Memory types: user, feedback, project, reference.
140
218
  DESC
141
219
  example %(echo "deep Go expertise, new to React" | llm memory write user-role --type=user --description="user profile")
220
+ # :name first — the parser fills scalar opts from positionals in
221
+ # declaration order, so without it `llm memory write foo --type=user`
222
+ # lands "foo" in :description instead of the memory name.
223
+ opt :name, desc: 'memory name (slug)'
142
224
  opt :type, desc: 'memory type (user|feedback|project|reference)', req: true
143
225
  opt :description, desc: 'one-line summary stored in frontmatter'
144
226
 
145
227
  proc do |opts|
146
- name = opts[:args].first
228
+ name = opts[:name]
147
229
  error 'usage: llm memory write <name> --type=<type> [--description="..."] < body' unless name
148
230
  error "unknown type: #{opts[:type]} (valid: #{VALID_TYPES.join(', ')})" unless VALID_TYPES.include?(opts[:type])
149
231
 
150
- body = $stdin.read
232
+ # opts[:stdin], not $stdin.read — hammer drains piped stdin into opts
233
+ # before the handler runs, so a direct read comes back empty.
234
+ body = opts[:stdin].to_s
151
235
  error 'body is empty (pipe content on stdin)' if body.strip.empty?
152
236
 
153
237
  path = memory_path(name)
@@ -465,7 +549,7 @@ namespace :prompt do
465
549
  proc do |opts|
466
550
  error '--claude or --codex required' unless opts[:claude] || opts[:codex]
467
551
 
468
- raw = $stdin.read
552
+ raw = opts[:stdin].to_s
469
553
  prompt = begin
470
554
  JSON.parse(raw).fetch('prompt', raw)
471
555
  rescue JSON::ParserError
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.17
4
+ version: 0.3.21
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-07-12 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -46,6 +46,7 @@ files:
46
46
  - "./lib/hammer/cron_server.rb"
47
47
  - "./lib/hammer/cron_web.rb"
48
48
  - "./lib/hammer/dotenv.rb"
49
+ - "./lib/hammer/input.rb"
49
50
  - "./lib/hammer/loader.rb"
50
51
  - "./lib/hammer/option.rb"
51
52
  - "./lib/hammer/parser.rb"
@@ -54,16 +55,8 @@ files:
54
55
  - "./lib/lux-hammer.rb"
55
56
  - "./recipes/deploy.rb"
56
57
  - "./recipes/git-helper.rb"
57
- - "./recipes/lib/deploy/boot.rb"
58
- - "./recipes/lib/deploy/commands.rb"
59
- - "./recipes/lib/deploy/config.rb"
60
- - "./recipes/lib/deploy/context.rb"
61
- - "./recipes/lib/deploy/doctor.rb"
62
- - "./recipes/lib/deploy/hammer.rb"
63
- - "./recipes/lib/deploy/manifest.rb"
64
- - "./recipes/lib/deploy/ssh.rb"
65
- - "./recipes/lib/deploy/template.rb"
66
58
  - "./recipes/lib/llm/usage.rb"
59
+ - "./recipes/lib/llm/wrap.rb"
67
60
  - "./recipes/llm.rb"
68
61
  - "./recipes/srt.rb"
69
62
  - bin/hammer
@@ -1,52 +0,0 @@
1
- require 'fileutils'
2
- require 'pathname'
3
- require 'open3'
4
- require 'set'
5
- require 'shellwords'
6
- require 'yaml'
7
-
8
- module LuxDeploy
9
- # Recipe layout: this file and its siblings live under
10
- # recipes/lib/deploy/, so ROOT points here and `templates/` sits
11
- # right next to us (no gem root anymore).
12
- ROOT ||= Pathname.new(__dir__)
13
- VERSION ||= '0.2.0'
14
-
15
- # Branches that select `.env` instead of `.env.staging`.
16
- MAIN_BRANCHES ||= %w[master main]
17
-
18
- # Server-side conventions. Not config-tunable because doctor and the
19
- # deploy flow both hardcode these paths in the host setup. A different
20
- # caddy/systemd layout means a different recipe.
21
- PORT_RANGE ||= (3010..3990).step(10).to_a
22
- CADDY_SITES ||= '/etc/caddy/sites'
23
- SYSTEMD_DIR ||= '/etc/systemd/system'
24
-
25
- class Error < StandardError
26
- def to_s
27
- "ERROR: #{super}"
28
- end
29
- end
30
-
31
- # Host-supplied defaults that sit under the user's .yaml. Set once by a
32
- # wrapping plugin/Hammerfile (e.g. lux-fw seeds 'lux-web' / 'lux-apps'),
33
- # consumed by Config.new. Empty by default so the recipe stays "generic".
34
- @defaults = {}
35
-
36
- class << self
37
- attr_reader :defaults
38
-
39
- def set_defaults(hash)
40
- @defaults = (hash || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v }
41
- end
42
- end
43
- end
44
-
45
- require_relative 'config'
46
- require_relative 'ssh'
47
- require_relative 'template'
48
- require_relative 'doctor'
49
- require_relative 'context'
50
- require_relative 'manifest'
51
- require_relative 'commands'
52
- require_relative 'hammer'