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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/AGENTS.md +16 -6
- data/README.md +83 -13
- data/lib/hammer/builder.rb +6 -1
- data/lib/hammer/input.rb +121 -0
- data/lib/hammer/option.rb +23 -9
- data/lib/hammer/parser.rb +12 -14
- data/lib/lux-hammer.rb +98 -16
- data/recipes/deploy.rb +15 -7
- data/recipes/lib/llm/usage.rb +353 -16
- data/recipes/lib/llm/wrap.rb +882 -0
- data/recipes/llm.rb +97 -13
- metadata +4 -11
- data/recipes/lib/deploy/boot.rb +0 -52
- data/recipes/lib/deploy/commands.rb +0 -555
- data/recipes/lib/deploy/config.rb +0 -62
- data/recipes/lib/deploy/context.rb +0 -149
- data/recipes/lib/deploy/doctor.rb +0 -238
- data/recipes/lib/deploy/hammer.rb +0 -168
- data/recipes/lib/deploy/manifest.rb +0 -169
- data/recipes/lib/deploy/ssh.rb +0 -129
- data/recipes/lib/deploy/template.rb +0 -39
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
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
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[:
|
|
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(
|
|
68
|
+
puts JSON.generate(
|
|
69
|
+
LlmUsage.rows_to_json(rows, period: period, notes: notes, token_rows: token_rows)
|
|
70
|
+
)
|
|
58
71
|
else
|
|
59
|
-
|
|
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[:
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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-
|
|
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
|
data/recipes/lib/deploy/boot.rb
DELETED
|
@@ -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'
|