lux-hammer 0.3.29 → 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/wrap.rb +16 -5
- data/recipes/llm.rb +25 -14
- 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/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
|
|
@@ -207,6 +207,11 @@ namespace :wrap do
|
|
|
207
207
|
approvals-and-sandbox, --always-approve, --auto
|
|
208
208
|
-b bare - no AGENTS.md injection skips the default below
|
|
209
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
|
+
|
|
210
215
|
By default every AGENTS.md from ~ down to cwd is handed to the tool: claude
|
|
211
216
|
--append-system-prompt, codex -c developer_instructions, grok --rules, opencode
|
|
212
217
|
inline config. codex, grok and opencode read AGENTS.md from the git root down by
|
|
@@ -217,10 +222,12 @@ namespace :wrap do
|
|
|
217
222
|
example 'wrap:run g -cf'
|
|
218
223
|
example 'wrap:run cod -cb'
|
|
219
224
|
example 'wrap:run c -- --model opus'
|
|
225
|
+
example 'wrap:run --update'
|
|
220
226
|
|
|
221
227
|
opt :continue, type: :boolean, desc: 'continue the last session in this folder'
|
|
222
228
|
opt :full, type: :boolean, desc: 'full permissions, no approval prompts'
|
|
223
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'
|
|
224
231
|
|
|
225
232
|
proc do |opts|
|
|
226
233
|
require File.join(_llm_root, 'lib/llm/wrap')
|
|
@@ -232,6 +239,11 @@ namespace :wrap do
|
|
|
232
239
|
error("unknown tool #{prefix.inspect} - one of #{LlmLaunch::TOOLS.keys.join(', ')}")
|
|
233
240
|
extra = prefix ? args.drop(1) : args
|
|
234
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
|
+
|
|
235
247
|
launch = LlmLaunch.build(tool, extra, continue: opts[:continue], full: opts[:full], bare: opts[:bare])
|
|
236
248
|
ENV.update(launch.env)
|
|
237
249
|
say.gray "agents: #{launch.files.map { |f| f.to_s.sub(Dir.home, '~') }.join(', ')}" if launch.files.any?
|
|
@@ -486,7 +498,7 @@ end
|
|
|
486
498
|
|
|
487
499
|
namespace :prompt do
|
|
488
500
|
TOKEN_PATTERN ||= /[a-z0-9_-]+/.freeze
|
|
489
|
-
TOKEN_LINE_RE ||= /\A(?:\s
|
|
501
|
+
TOKEN_LINE_RE ||= /\A(?:\s*::[a-z0-9_-]+)+\s*\z/.freeze
|
|
490
502
|
HOOK_EVENT ||= 'UserPromptSubmit'
|
|
491
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."
|
|
492
504
|
QUESTION_RULE ||= <<~RULE.strip
|
|
@@ -508,8 +520,8 @@ namespace :prompt do
|
|
|
508
520
|
def tokens_in(input)
|
|
509
521
|
out = []
|
|
510
522
|
scanner = input.to_s.strip
|
|
511
|
-
while (m = scanner.match(/\A(
|
|
512
|
-
out <<
|
|
523
|
+
while (m = scanner.match(/\A::(#{TOKEN_PATTERN})(?=\s|$)/))
|
|
524
|
+
out << m[1]
|
|
513
525
|
scanner = scanner[m[0].length..].to_s.lstrip
|
|
514
526
|
end
|
|
515
527
|
out.uniq
|
|
@@ -531,7 +543,7 @@ namespace :prompt do
|
|
|
531
543
|
ordered = folders.sort_by { |label, _| label == 'global' ? 0 : 1 }
|
|
532
544
|
ordered.map do |label, folder|
|
|
533
545
|
toks = Dir.glob(File.join(folder, '*.md')).map { |p| File.basename(p, '.md') }.sort
|
|
534
|
-
items = toks.empty? ? '(none)' : toks.map { |t| "
|
|
546
|
+
items = toks.empty? ? '(none)' : toks.map { |t| "::#{t}" }.join(', ')
|
|
535
547
|
"Available #{label} in #{display_path(folder)} -> #{items}"
|
|
536
548
|
end.join("\n")
|
|
537
549
|
end
|
|
@@ -549,7 +561,7 @@ namespace :prompt do
|
|
|
549
561
|
name = File.basename(path, '.md')
|
|
550
562
|
dscr = first_line_description(path)
|
|
551
563
|
dscr = '(no description)' if dscr.empty?
|
|
552
|
-
"
|
|
564
|
+
" ::#{name.ljust(width)} #{dscr}"
|
|
553
565
|
end
|
|
554
566
|
"#{header}\n#{entries.join("\n")}"
|
|
555
567
|
end
|
|
@@ -619,7 +631,7 @@ namespace :prompt do
|
|
|
619
631
|
end
|
|
620
632
|
|
|
621
633
|
break unless stripped =~ TOKEN_LINE_RE
|
|
622
|
-
prefix_tokens.concat(stripped.scan(
|
|
634
|
+
prefix_tokens.concat(stripped.scan(/::(#{TOKEN_PATTERN})/).flatten)
|
|
623
635
|
remaining = rest.to_s
|
|
624
636
|
end
|
|
625
637
|
|
|
@@ -640,9 +652,9 @@ namespace :prompt do
|
|
|
640
652
|
end
|
|
641
653
|
|
|
642
654
|
def load_command_content(token, seen)
|
|
643
|
-
error "circular include of
|
|
655
|
+
error "circular include of ::#{token}" if seen.include?(token)
|
|
644
656
|
path = find_command_path(token)
|
|
645
|
-
error %(custom token "
|
|
657
|
+
error %(custom token "::#{token}" not found.\n\n#{grouped_listing}) unless path
|
|
646
658
|
|
|
647
659
|
child_seen = seen + [token]
|
|
648
660
|
content = File.read(path)
|
|
@@ -671,7 +683,7 @@ namespace :prompt do
|
|
|
671
683
|
seen = Set.new
|
|
672
684
|
loaded = toks.map do |token|
|
|
673
685
|
path = find_command_path(token)
|
|
674
|
-
error %(custom token "
|
|
686
|
+
error %(custom token "::#{token}" not found.\n\n#{grouped_listing}) unless path
|
|
675
687
|
[token, Pathname.new(path).cleanpath.to_s, load_command_content(token, seen)]
|
|
676
688
|
end
|
|
677
689
|
|
|
@@ -721,12 +733,11 @@ namespace :prompt do
|
|
|
721
733
|
|
|
722
734
|
task :expand do
|
|
723
735
|
desc 'Expand prompt token(s) and print the resulting context'
|
|
724
|
-
example 'prompt:expand
|
|
725
|
-
example 'prompt:expand foo:'
|
|
736
|
+
example 'prompt:expand ::foo ::bar'
|
|
726
737
|
|
|
727
738
|
proc do |opts|
|
|
728
739
|
input = opts[:args].join(' ')
|
|
729
|
-
error 'usage: llm prompt:expand
|
|
740
|
+
error 'usage: llm prompt:expand ::token [::token ...]' if input.empty?
|
|
730
741
|
out = load_context(input)
|
|
731
742
|
say out unless out.empty?
|
|
732
743
|
end
|
|
@@ -735,7 +746,7 @@ namespace :prompt do
|
|
|
735
746
|
task :hook do
|
|
736
747
|
desc <<~D
|
|
737
748
|
UserPromptSubmit hook entry. Reads {"prompt": ...} JSON on stdin,
|
|
738
|
-
expands any token prefix, and emits hookSpecificOutput JSON on stdout.
|
|
749
|
+
expands any ::token prefix, and emits hookSpecificOutput JSON on stdout.
|
|
739
750
|
|
|
740
751
|
Pair with HAMMER_QUIET=1 in the hook command so the runtime banner
|
|
741
752
|
doesn't pollute stdout.
|
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"
|