lux-hammer 0.3.28 → 0.3.30
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/lib/lux-hammer.rb +4 -2
- data/recipes/git-helper.rb +20 -0
- data/recipes/lib/llm/launch.rb +7 -0
- data/recipes/lib/llm/update.rb +140 -0
- data/recipes/lib/llm/usage.rb +52 -9
- data/recipes/lib/llm/wrap.rb +16 -5
- data/recipes/llm.rb +31 -17
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ca175d7445c386d0c8bd3be6834b057e6b958ee3d0de48d72e49a53ef4da776
|
|
4
|
+
data.tar.gz: 3303a15ee74702d81ebfba70d15366870bfb8dccc2d5b0fc8c5304e2fe28ef0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d925611cf831aa0e6c4198d4761bdcb04b7454afab82a8cc12100130e23dc63139f3bad8fbf0473cd67f7bf71ea46ef3ceeb91e266f6bddba51f4097469231dc
|
|
7
|
+
data.tar.gz: a489e910f4027422474b1741a0ff45e4e5729009af2d5c6ba7fbf79a7f11661e547b9947058c6b5c237e41fbecfc5baa9a2a2686158a95460a0c2be7ba4bf329
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.30
|
data/lib/lux-hammer.rb
CHANGED
|
@@ -1281,10 +1281,12 @@ class Hammer
|
|
|
1281
1281
|
# built-ins (so the script's `--help` shows only what it defines).
|
|
1282
1282
|
# `program_name` is the script's basename so help reads "myscript foo"
|
|
1283
1283
|
# rather than "hammer foo" - works even when invoked via a symlink in
|
|
1284
|
-
# PATH, since argv[0] is the path the user typed.
|
|
1284
|
+
# PATH, since argv[0] is the path the user typed. `.rb` is stripped so
|
|
1285
|
+
# a recipe exec'd as `recipes/llm.rb` (typical `~/bin/llm` stub) still
|
|
1286
|
+
# banners as `llm`, matching `Hammer.recipe(:llm)`.
|
|
1285
1287
|
def self.run_shebang(path, argv)
|
|
1286
1288
|
klass = Class.new(Hammer)
|
|
1287
|
-
klass.instance_variable_set(:@program_name, File.basename(path))
|
|
1289
|
+
klass.instance_variable_set(:@program_name, File.basename(path, '.rb'))
|
|
1288
1290
|
Builder.new(klass).evaluate(File.read(path), path)
|
|
1289
1291
|
klass.start(argv)
|
|
1290
1292
|
end
|
data/recipes/git-helper.rb
CHANGED
|
@@ -450,6 +450,26 @@ task :pr do
|
|
|
450
450
|
proc { open_in_browser(remote_pr_url) }
|
|
451
451
|
end
|
|
452
452
|
|
|
453
|
+
task :prs do
|
|
454
|
+
desc 'list open PRs: id, author, short title, url (others first, then mine)'
|
|
455
|
+
proc do |_|
|
|
456
|
+
require 'json'
|
|
457
|
+
out = `GH_PAGER=cat gh pr list --state open --limit 100 --json number,title,url,author`
|
|
458
|
+
error 'gh pr list failed' unless $?.success?
|
|
459
|
+
prs = JSON.parse(out)
|
|
460
|
+
next say('No open PRs', :yellow) if prs.empty?
|
|
461
|
+
me = `GH_PAGER=cat gh api user --jq .login`.chomp
|
|
462
|
+
mine, others = prs.partition { |pr| pr.dig('author', 'login') == me }
|
|
463
|
+
author_width = prs.map { |pr| pr.dig('author', 'login').to_s.length }.max
|
|
464
|
+
(others + mine).each do |pr|
|
|
465
|
+
title = pr['title'].length > 50 ? "#{pr['title'][0, 49]}…" : pr['title']
|
|
466
|
+
author = pr.dig('author', 'login').to_s.ljust(author_width)
|
|
467
|
+
author = author.color(mine.include?(pr) ? :green : :blue)
|
|
468
|
+
puts "#{"##{pr['number']}".ljust(7).color(:yellow)} #{author} #{title.ljust(50)} #{pr['url']}"
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
453
473
|
# ---- tags / users / stats --------------------------------------------
|
|
454
474
|
|
|
455
475
|
task :tag do
|
data/recipes/lib/llm/launch.rb
CHANGED
|
@@ -9,17 +9,24 @@ require 'pathname'
|
|
|
9
9
|
module LlmLaunch
|
|
10
10
|
Launch = Struct.new(:argv, :env, :files)
|
|
11
11
|
|
|
12
|
+
# The one registry of agent CLIs. :continue / :full / :agents drive a launch;
|
|
13
|
+
# :update / :version / :models drive `llm wrap:run --update` (LlmUpdate).
|
|
12
14
|
# :agents gets the combined AGENTS.md text and the file paths, and answers
|
|
13
15
|
# with the argv to add and/or env to set - opencode has no flag for
|
|
14
16
|
# instructions, only inline config, and that one takes paths.
|
|
17
|
+
# :models is nil where the CLI has no listing command - skip, not "none".
|
|
15
18
|
TOOLS ||= {
|
|
16
19
|
'claude' => { continue: %w[-c], full: %w[--dangerously-skip-permissions],
|
|
20
|
+
update: %w[claude update], version: %w[claude --version], models: nil,
|
|
17
21
|
agents: ->(text, _files) { { args: ['--append-system-prompt', text] } } },
|
|
18
22
|
'codex' => { continue: %w[resume --last], full: %w[--dangerously-bypass-approvals-and-sandbox],
|
|
23
|
+
update: %w[codex update], version: %w[codex --version], models: nil,
|
|
19
24
|
agents: ->(text, _files) { { args: ['-c', "developer_instructions=#{text}"] } } },
|
|
20
25
|
'grok' => { continue: %w[-c], full: %w[--always-approve],
|
|
26
|
+
update: %w[grok update], version: %w[grok version], models: %w[grok models],
|
|
21
27
|
agents: ->(text, _files) { { args: ['--rules', text] } } },
|
|
22
28
|
'opencode' => { continue: %w[-c], full: %w[--auto],
|
|
29
|
+
update: %w[opencode upgrade], version: %w[opencode --version], models: %w[opencode models],
|
|
23
30
|
agents: ->(_text, files) { { env: { 'OPENCODE_CONFIG_CONTENT' => { instructions: files }.to_json } } } },
|
|
24
31
|
}.freeze
|
|
25
32
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative 'launch'
|
|
6
|
+
|
|
7
|
+
# Backs `llm wrap:run --update` (`ai --update`): runs each agent CLI's own
|
|
8
|
+
# updater and reports old -> new, then re-reads the model lists and shows what
|
|
9
|
+
# appeared or disappeared since the last run. None of these tools download
|
|
10
|
+
# model weights - the models are API-side, so the list is the thing that moves.
|
|
11
|
+
module LlmUpdate
|
|
12
|
+
extend Hammer::Shell
|
|
13
|
+
|
|
14
|
+
CACHE_PATH ||= File.expand_path('~/.cache/llm/models.json')
|
|
15
|
+
WIDTH ||= LlmLaunch::TOOLS.keys.map(&:size).max
|
|
16
|
+
|
|
17
|
+
# How each CLI prints its list: grok bullets it under a preamble ("You are
|
|
18
|
+
# not authenticated." when logged out) and marks the default, opencode prints
|
|
19
|
+
# one bare provider/model per line.
|
|
20
|
+
MODEL_RE ||= {
|
|
21
|
+
'grok' => /\A\s*[*-]\s+(\S+)/,
|
|
22
|
+
'opencode' => %r{\A(\S+/\S+)\z},
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Returns the exit status - non-zero if any updater failed.
|
|
28
|
+
def run(tools)
|
|
29
|
+
rows = tools.map { |tool| update_one(tool) }
|
|
30
|
+
say ''
|
|
31
|
+
rows.each { |row| report(row) }
|
|
32
|
+
report_models(rows.select { |row| row[:installed] }.map { |row| row[:tool] })
|
|
33
|
+
rows.any? { |row| row[:failed] } ? 1 : 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update_one(tool)
|
|
37
|
+
return { tool: tool, installed: false } unless command?(tool)
|
|
38
|
+
|
|
39
|
+
argv = LlmLaunch::TOOLS.fetch(tool)[:update]
|
|
40
|
+
before = version(tool)
|
|
41
|
+
say.gray "$ #{argv.join(' ')}"
|
|
42
|
+
ok = system(*argv)
|
|
43
|
+
{ tool: tool, installed: true, before: before, after: version(tool), failed: !ok }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def report(row)
|
|
47
|
+
name = row[:tool].ljust(WIDTH)
|
|
48
|
+
return say.gray("* #{name} not installed") unless row[:installed]
|
|
49
|
+
return say.red("* #{name} update failed") if row[:failed]
|
|
50
|
+
|
|
51
|
+
before, after = row.values_at(:before, :after)
|
|
52
|
+
if before && after && before != after
|
|
53
|
+
say.green "* #{name} #{before} -> #{after}"
|
|
54
|
+
elsif after
|
|
55
|
+
say "* #{name} #{after} (latest)"
|
|
56
|
+
else
|
|
57
|
+
say "* #{name} #{before || 'unknown version'}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def report_models(tools)
|
|
62
|
+
tools = tools.select { |tool| LlmLaunch::TOOLS.fetch(tool)[:models] }
|
|
63
|
+
return if tools.empty?
|
|
64
|
+
|
|
65
|
+
cache = cache_read
|
|
66
|
+
say ''
|
|
67
|
+
say 'models'
|
|
68
|
+
tools.each { |tool| cache[tool] = report_model_list(tool, cache[tool]) }
|
|
69
|
+
cache_write(cache)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Prints one tool's list against the previously cached one, and answers with
|
|
73
|
+
# the fresh list so the caller can write it back.
|
|
74
|
+
def report_model_list(tool, old)
|
|
75
|
+
list = models(tool)
|
|
76
|
+
name = tool.ljust(WIDTH)
|
|
77
|
+
if list.empty?
|
|
78
|
+
say.gray " #{name} no models listed"
|
|
79
|
+
return old || []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
say " #{name} #{list.size} models"
|
|
83
|
+
unless old
|
|
84
|
+
say.gray ' (first run)'
|
|
85
|
+
return list
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
added, removed = diff(old, list)
|
|
89
|
+
say.gray(' unchanged') if added.empty? && removed.empty?
|
|
90
|
+
added.each { |model| say.green " + #{model}" }
|
|
91
|
+
removed.each { |model| say.gray " - #{model}" }
|
|
92
|
+
list
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def version(tool)
|
|
96
|
+
parse_version(capture(LlmLaunch::TOOLS.fetch(tool)[:version]))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def models(tool)
|
|
100
|
+
parse_models(tool, capture(LlmLaunch::TOOLS.fetch(tool)[:models]))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# `2.1.268 (Claude Code)`, `codex-cli 0.154.0`, `grok 1.0.25 (f7e67d6) [stable]`, `1.18.30`
|
|
104
|
+
def parse_version(out)
|
|
105
|
+
out.to_s.lines.first.to_s[/\d+\.\d+(?:\.\d+)*/]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_models(tool, out)
|
|
109
|
+
re = MODEL_RE[tool]
|
|
110
|
+
return [] unless re
|
|
111
|
+
out.to_s.lines.filter_map { |line| line.rstrip[re, 1] }.uniq.sort
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def diff(old, new)
|
|
115
|
+
[new - old, old - new]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def cache_read
|
|
119
|
+
JSON.parse(File.read(CACHE_PATH))
|
|
120
|
+
rescue StandardError
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def cache_write(data)
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(CACHE_PATH))
|
|
126
|
+
tmp = "#{CACHE_PATH}.#{Process.pid}.tmp"
|
|
127
|
+
File.write(tmp, JSON.pretty_generate(data))
|
|
128
|
+
File.rename(tmp, CACHE_PATH)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def capture(argv)
|
|
132
|
+
IO.popen(argv, err: %i[child out], &:read).to_s
|
|
133
|
+
rescue StandardError
|
|
134
|
+
''
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def command?(name)
|
|
138
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) }
|
|
139
|
+
end
|
|
140
|
+
end
|
data/recipes/lib/llm/usage.rb
CHANGED
|
@@ -64,7 +64,7 @@ module LlmUsage
|
|
|
64
64
|
# and caching would freeze the staleness note. Only the API call caches.
|
|
65
65
|
data, note = fetch_claude_usage(period: label, cache: cache, now: now)
|
|
66
66
|
notes << note unless note.nil? || note.empty?
|
|
67
|
-
rows.concat(parse_claude(data, now: now)) if data
|
|
67
|
+
rows.concat(parse_claude(data, now: now, period: label)) if data
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
if wanted.include?(:codex)
|
|
@@ -294,7 +294,7 @@ module LlmUsage
|
|
|
294
294
|
"#{mins}m"
|
|
295
295
|
end
|
|
296
296
|
|
|
297
|
-
def parse_claude(data, now: Time.now)
|
|
297
|
+
def parse_claude(data, now: Time.now, period: nil)
|
|
298
298
|
return [] unless data.is_a?(Hash)
|
|
299
299
|
|
|
300
300
|
five = data['five_hour']
|
|
@@ -329,13 +329,13 @@ module LlmUsage
|
|
|
329
329
|
month_reset: '-'
|
|
330
330
|
)
|
|
331
331
|
end
|
|
332
|
-
return rows
|
|
332
|
+
return rows + claude_scoped_rows(data['limits'], rows, now: now, period: period)
|
|
333
333
|
end
|
|
334
334
|
|
|
335
335
|
week = data['seven_day']
|
|
336
336
|
return [] unless five.is_a?(Hash) || week.is_a?(Hash)
|
|
337
337
|
|
|
338
|
-
[
|
|
338
|
+
rows = [
|
|
339
339
|
UsageRow.new(
|
|
340
340
|
name: 'Claude',
|
|
341
341
|
session_pct: format_pct(session_pct),
|
|
@@ -346,6 +346,36 @@ module LlmUsage
|
|
|
346
346
|
month_reset: month[:reset]
|
|
347
347
|
)
|
|
348
348
|
]
|
|
349
|
+
rows + claude_scoped_rows(data['limits'], rows, now: now, period: period)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Per-model weekly buckets live in `limits` as kind weekly_scoped - Fable has
|
|
353
|
+
# one, and seven_day_opus/_sonnet read null on the accounts that do. Named off
|
|
354
|
+
# the API's own display_name, so a model added later needs nothing here; a
|
|
355
|
+
# scope already carrying its own seven_day_* row is skipped rather than
|
|
356
|
+
# doubled. A weekly bucket has nothing to say in the month view, where these
|
|
357
|
+
# would be a row of dashes.
|
|
358
|
+
def claude_scoped_rows(limits, rows, now: Time.now, period: nil)
|
|
359
|
+
return [] if period_label(period) == 'month'
|
|
360
|
+
|
|
361
|
+
taken = rows.map(&:name)
|
|
362
|
+
|
|
363
|
+
Array(limits).filter_map do |limit|
|
|
364
|
+
next unless limit.is_a?(Hash) && limit['kind'] == 'weekly_scoped'
|
|
365
|
+
|
|
366
|
+
name = limit.dig('scope', 'model', 'display_name')
|
|
367
|
+
next if name.nil? || name.empty? || taken.include?(name)
|
|
368
|
+
|
|
369
|
+
UsageRow.new(
|
|
370
|
+
name: name,
|
|
371
|
+
session_pct: '-',
|
|
372
|
+
session_reset: '-',
|
|
373
|
+
week_pct: format_pct(limit['percent']),
|
|
374
|
+
week_reset: format_reset_short(limit['resets_at'], now: now),
|
|
375
|
+
month_pct: '-',
|
|
376
|
+
month_reset: '-'
|
|
377
|
+
)
|
|
378
|
+
end
|
|
349
379
|
end
|
|
350
380
|
|
|
351
381
|
def parse_codex(data, now: Time.now)
|
|
@@ -403,7 +433,7 @@ module LlmUsage
|
|
|
403
433
|
return fetch_cached(:claude, cache) { fetch_claude_oauth_usage } if period == 'month'
|
|
404
434
|
|
|
405
435
|
data, note = fetch_claude_snapshot(now: now, path: path)
|
|
406
|
-
return [data, note] if data
|
|
436
|
+
return [merge_scoped_limits(data, cache: cache), note] if data
|
|
407
437
|
|
|
408
438
|
api_data, api_note = fetch_cached(:claude, cache) { fetch_claude_oauth_usage }
|
|
409
439
|
return [api_data, api_note] if api_data
|
|
@@ -411,6 +441,18 @@ module LlmUsage
|
|
|
411
441
|
[nil, [note, api_note].compact.join('; ')]
|
|
412
442
|
end
|
|
413
443
|
|
|
444
|
+
# The statusline snapshot carries no per-model windows - Claude Code's
|
|
445
|
+
# `rate_limits` input is five_hour/seven_day and nothing else - so borrow
|
|
446
|
+
# `limits` from the API payload. Cached, so this is one call per CACHE_TTL,
|
|
447
|
+
# and silent when it fails: the snapshot's own numbers are still good.
|
|
448
|
+
def merge_scoped_limits(data, cache: true)
|
|
449
|
+
api, = fetch_cached(:claude, cache) { fetch_claude_oauth_usage }
|
|
450
|
+
limits = api['limits'] if api.is_a?(Hash)
|
|
451
|
+
return data unless limits
|
|
452
|
+
|
|
453
|
+
data.merge('limits' => limits)
|
|
454
|
+
end
|
|
455
|
+
|
|
414
456
|
def fetch_claude_snapshot(now: Time.now, path: CLAUDE_LIMITS_PATH)
|
|
415
457
|
snapshot = read_json(path)
|
|
416
458
|
return [nil, 'claude: no local snapshot (statusline writer not installed)'] unless snapshot.is_a?(Hash)
|
|
@@ -685,11 +727,12 @@ module LlmUsage
|
|
|
685
727
|
end
|
|
686
728
|
end
|
|
687
729
|
|
|
730
|
+
# A failure is cached too, or an unreachable endpoint costs its timeout on
|
|
731
|
+
# every run - and the snapshot path that now consults the API is the one
|
|
732
|
+
# that used to be instant and offline.
|
|
688
733
|
data, note = yield
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
File.write(path, JSON.generate('fetched_at' => Time.now.iso8601, 'data' => data, 'note' => note))
|
|
692
|
-
end
|
|
734
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
735
|
+
File.write(path, JSON.generate('fetched_at' => Time.now.iso8601, 'data' => data, 'note' => note))
|
|
693
736
|
[data, note]
|
|
694
737
|
end
|
|
695
738
|
|
data/recipes/lib/llm/wrap.rb
CHANGED
|
@@ -673,10 +673,12 @@ module LlmWrap
|
|
|
673
673
|
# * a sequence, leaving the introducer stranded and its tail printed as
|
|
674
674
|
# text - the literal "[22m" on screen
|
|
675
675
|
# * a UTF-8 character, which comes out as a replacement glyph
|
|
676
|
-
# * a
|
|
677
|
-
#
|
|
678
|
-
#
|
|
679
|
-
#
|
|
676
|
+
# * a cursor save the child opened and has not restored yet, in either the
|
|
677
|
+
# DECSC (ESC7/ESC8) or SCO (CSI s / CSI u) form. The terminal has one save
|
|
678
|
+
# slot; we take it, and the child's own restore then puts its cursor on our
|
|
679
|
+
# bar and it draws the rest of the frame over the top. Claude Code brackets
|
|
680
|
+
# every repaint in ESC7 ... ESC8 and OpenCode uses CSI s ... CSI u, so this
|
|
681
|
+
# one is not theoretical.
|
|
680
682
|
#
|
|
681
683
|
# This is only ever asked about the *end* of what we have written so far, so
|
|
682
684
|
# there is no need to understand the sequences - just to know where they stop.
|
|
@@ -774,11 +776,20 @@ module LlmWrap
|
|
|
774
776
|
end
|
|
775
777
|
|
|
776
778
|
def csi(byte)
|
|
779
|
+
first = @len.zero?
|
|
777
780
|
@len += 1
|
|
778
781
|
return @state = :esc if byte == ESC # aborted, a new one starting
|
|
779
782
|
return @state = :text if @len > MAX_CSI
|
|
780
783
|
|
|
781
|
-
|
|
784
|
+
if byte >= 0x40 && byte <= 0x7e
|
|
785
|
+
# SCO save/restore (CSI s / CSI u) share the single save slot with
|
|
786
|
+
# DECSC/DECRC on the emulators we run against, and OpenCode repaints
|
|
787
|
+
# through this pair - a paint between them takes the slot and its
|
|
788
|
+
# restore lands the rest of the frame on our bar.
|
|
789
|
+
@saved += 1 if first && byte == 0x73
|
|
790
|
+
@saved -= 1 if first && byte == 0x75 && @saved.positive?
|
|
791
|
+
@state = :text
|
|
792
|
+
end
|
|
782
793
|
end
|
|
783
794
|
|
|
784
795
|
# String payloads run to BEL or ST (ESC \).
|
data/recipes/llm.rb
CHANGED
|
@@ -9,7 +9,7 @@ desc <<~TXT
|
|
|
9
9
|
browser drive a local Chrome with Stagehand, thinking through claude / codex / ollama
|
|
10
10
|
memory persistent memory store (backs the Claude Code memory plugin)
|
|
11
11
|
plan apply a /plan bundle - sha1 checked, drift aware
|
|
12
|
-
prompt token
|
|
12
|
+
prompt ::token prompt expander (UserPromptSubmit hook + CLI)
|
|
13
13
|
|
|
14
14
|
Commands:
|
|
15
15
|
usage subscription limits and per-model token usage
|
|
@@ -45,8 +45,10 @@ task :usage do
|
|
|
45
45
|
`~/.grok/logs/unified.jsonl` for the period end; tokens still sum
|
|
46
46
|
inference_done events in that log. Claude 5h/week windows come from the
|
|
47
47
|
statusline snapshot at `~/.cache/llm/claude-limits.json`, falling back to
|
|
48
|
-
the OAuth usage API.
|
|
49
|
-
|
|
48
|
+
the OAuth usage API. Per-model weekly windows (Fable) get their own row and
|
|
49
|
+
come from that API either way - the snapshot doesn't carry them. `month`
|
|
50
|
+
shows Claude extra-usage credits (API only, and only when you've enabled
|
|
51
|
+
them).
|
|
50
52
|
D
|
|
51
53
|
example 'usage'
|
|
52
54
|
example 'usage month'
|
|
@@ -205,6 +207,11 @@ namespace :wrap do
|
|
|
205
207
|
approvals-and-sandbox, --always-approve, --auto
|
|
206
208
|
-b bare - no AGENTS.md injection skips the default below
|
|
207
209
|
|
|
210
|
+
--update runs each CLI's own updater (claude/codex/grok update, opencode upgrade),
|
|
211
|
+
reports old -> new, then re-reads the model lists and shows what appeared or
|
|
212
|
+
disappeared since the last run. A TOOL prefix scopes it to that one tool; no prefix
|
|
213
|
+
updates all four.
|
|
214
|
+
|
|
208
215
|
By default every AGENTS.md from ~ down to cwd is handed to the tool: claude
|
|
209
216
|
--append-system-prompt, codex -c developer_instructions, grok --rules, opencode
|
|
210
217
|
inline config. codex, grok and opencode read AGENTS.md from the git root down by
|
|
@@ -215,10 +222,12 @@ namespace :wrap do
|
|
|
215
222
|
example 'wrap:run g -cf'
|
|
216
223
|
example 'wrap:run cod -cb'
|
|
217
224
|
example 'wrap:run c -- --model opus'
|
|
225
|
+
example 'wrap:run --update'
|
|
218
226
|
|
|
219
227
|
opt :continue, type: :boolean, desc: 'continue the last session in this folder'
|
|
220
228
|
opt :full, type: :boolean, desc: 'full permissions, no approval prompts'
|
|
221
229
|
opt :bare, type: :boolean, desc: 'bare agent - do not inject AGENTS.md'
|
|
230
|
+
opt :update, type: :boolean, desc: 'update the agent CLIs, then show model changes'
|
|
222
231
|
|
|
223
232
|
proc do |opts|
|
|
224
233
|
require File.join(_llm_root, 'lib/llm/wrap')
|
|
@@ -230,6 +239,11 @@ namespace :wrap do
|
|
|
230
239
|
error("unknown tool #{prefix.inspect} - one of #{LlmLaunch::TOOLS.keys.join(', ')}")
|
|
231
240
|
extra = prefix ? args.drop(1) : args
|
|
232
241
|
|
|
242
|
+
if opts[:update]
|
|
243
|
+
require File.join(_llm_root, 'lib/llm/update')
|
|
244
|
+
exit LlmUpdate.run(prefix ? [tool] : LlmLaunch::TOOLS.keys)
|
|
245
|
+
end
|
|
246
|
+
|
|
233
247
|
launch = LlmLaunch.build(tool, extra, continue: opts[:continue], full: opts[:full], bare: opts[:bare])
|
|
234
248
|
ENV.update(launch.env)
|
|
235
249
|
say.gray "agents: #{launch.files.map { |f| f.to_s.sub(Dir.home, '~') }.join(', ')}" if launch.files.any?
|
|
@@ -484,7 +498,7 @@ end
|
|
|
484
498
|
|
|
485
499
|
namespace :prompt do
|
|
486
500
|
TOKEN_PATTERN ||= /[a-z0-9_-]+/.freeze
|
|
487
|
-
TOKEN_LINE_RE ||= /\A(?:\s
|
|
501
|
+
TOKEN_LINE_RE ||= /\A(?:\s*::[a-z0-9_-]+)+\s*\z/.freeze
|
|
488
502
|
HOOK_EVENT ||= 'UserPromptSubmit'
|
|
489
503
|
VERBATIM_INSTRUCTION ||= "INSTRUCTION TO ASSISTANT: Do not answer the user's prompt. Print the message below verbatim to the user, preserving every line exactly as written. Do not summarize, truncate, or paraphrase."
|
|
490
504
|
QUESTION_RULE ||= <<~RULE.strip
|
|
@@ -506,8 +520,8 @@ namespace :prompt do
|
|
|
506
520
|
def tokens_in(input)
|
|
507
521
|
out = []
|
|
508
522
|
scanner = input.to_s.strip
|
|
509
|
-
while (m = scanner.match(/\A(
|
|
510
|
-
out <<
|
|
523
|
+
while (m = scanner.match(/\A::(#{TOKEN_PATTERN})(?=\s|$)/))
|
|
524
|
+
out << m[1]
|
|
511
525
|
scanner = scanner[m[0].length..].to_s.lstrip
|
|
512
526
|
end
|
|
513
527
|
out.uniq
|
|
@@ -529,7 +543,7 @@ namespace :prompt do
|
|
|
529
543
|
ordered = folders.sort_by { |label, _| label == 'global' ? 0 : 1 }
|
|
530
544
|
ordered.map do |label, folder|
|
|
531
545
|
toks = Dir.glob(File.join(folder, '*.md')).map { |p| File.basename(p, '.md') }.sort
|
|
532
|
-
items = toks.empty? ? '(none)' : toks.map { |t| "
|
|
546
|
+
items = toks.empty? ? '(none)' : toks.map { |t| "::#{t}" }.join(', ')
|
|
533
547
|
"Available #{label} in #{display_path(folder)} -> #{items}"
|
|
534
548
|
end.join("\n")
|
|
535
549
|
end
|
|
@@ -547,7 +561,7 @@ namespace :prompt do
|
|
|
547
561
|
name = File.basename(path, '.md')
|
|
548
562
|
dscr = first_line_description(path)
|
|
549
563
|
dscr = '(no description)' if dscr.empty?
|
|
550
|
-
"
|
|
564
|
+
" ::#{name.ljust(width)} #{dscr}"
|
|
551
565
|
end
|
|
552
566
|
"#{header}\n#{entries.join("\n")}"
|
|
553
567
|
end
|
|
@@ -617,7 +631,7 @@ namespace :prompt do
|
|
|
617
631
|
end
|
|
618
632
|
|
|
619
633
|
break unless stripped =~ TOKEN_LINE_RE
|
|
620
|
-
prefix_tokens.concat(stripped.scan(
|
|
634
|
+
prefix_tokens.concat(stripped.scan(/::(#{TOKEN_PATTERN})/).flatten)
|
|
621
635
|
remaining = rest.to_s
|
|
622
636
|
end
|
|
623
637
|
|
|
@@ -638,9 +652,9 @@ namespace :prompt do
|
|
|
638
652
|
end
|
|
639
653
|
|
|
640
654
|
def load_command_content(token, seen)
|
|
641
|
-
error "circular include of
|
|
655
|
+
error "circular include of ::#{token}" if seen.include?(token)
|
|
642
656
|
path = find_command_path(token)
|
|
643
|
-
error %(custom token "
|
|
657
|
+
error %(custom token "::#{token}" not found.\n\n#{grouped_listing}) unless path
|
|
644
658
|
|
|
645
659
|
child_seen = seen + [token]
|
|
646
660
|
content = File.read(path)
|
|
@@ -669,7 +683,7 @@ namespace :prompt do
|
|
|
669
683
|
seen = Set.new
|
|
670
684
|
loaded = toks.map do |token|
|
|
671
685
|
path = find_command_path(token)
|
|
672
|
-
error %(custom token "
|
|
686
|
+
error %(custom token "::#{token}" not found.\n\n#{grouped_listing}) unless path
|
|
673
687
|
[token, Pathname.new(path).cleanpath.to_s, load_command_content(token, seen)]
|
|
674
688
|
end
|
|
675
689
|
|
|
@@ -719,12 +733,11 @@ namespace :prompt do
|
|
|
719
733
|
|
|
720
734
|
task :expand do
|
|
721
735
|
desc 'Expand prompt token(s) and print the resulting context'
|
|
722
|
-
example 'prompt:expand
|
|
723
|
-
example 'prompt:expand foo:'
|
|
736
|
+
example 'prompt:expand ::foo ::bar'
|
|
724
737
|
|
|
725
738
|
proc do |opts|
|
|
726
739
|
input = opts[:args].join(' ')
|
|
727
|
-
error 'usage: llm prompt:expand
|
|
740
|
+
error 'usage: llm prompt:expand ::token [::token ...]' if input.empty?
|
|
728
741
|
out = load_context(input)
|
|
729
742
|
say out unless out.empty?
|
|
730
743
|
end
|
|
@@ -733,16 +746,17 @@ namespace :prompt do
|
|
|
733
746
|
task :hook do
|
|
734
747
|
desc <<~D
|
|
735
748
|
UserPromptSubmit hook entry. Reads {"prompt": ...} JSON on stdin,
|
|
736
|
-
expands any token prefix, and emits hookSpecificOutput JSON on stdout.
|
|
749
|
+
expands any ::token prefix, and emits hookSpecificOutput JSON on stdout.
|
|
737
750
|
|
|
738
751
|
Pair with HAMMER_QUIET=1 in the hook command so the runtime banner
|
|
739
752
|
doesn't pollute stdout.
|
|
740
753
|
D
|
|
741
754
|
opt :claude, type: :boolean, default: false, desc: 'Claude Code hook mode'
|
|
742
755
|
opt :codex, type: :boolean, default: false, desc: 'Codex hook mode'
|
|
756
|
+
opt :grok, type: :boolean, default: false, desc: 'Grok CLI hook mode'
|
|
743
757
|
|
|
744
758
|
proc do |opts|
|
|
745
|
-
error '--claude or --
|
|
759
|
+
error '--claude, --codex or --grok required' unless opts[:claude] || opts[:codex] || opts[:grok]
|
|
746
760
|
|
|
747
761
|
raw = opts[:stdin].to_s
|
|
748
762
|
prompt = begin
|
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.30
|
|
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-09-
|
|
11
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- "./recipes/lib/llm/browser.rb"
|
|
59
59
|
- "./recipes/lib/llm/launch.rb"
|
|
60
60
|
- "./recipes/lib/llm/plan.rb"
|
|
61
|
+
- "./recipes/lib/llm/update.rb"
|
|
61
62
|
- "./recipes/lib/llm/usage.rb"
|
|
62
63
|
- "./recipes/lib/llm/wrap.rb"
|
|
63
64
|
- "./recipes/llm.rb"
|