pwn 0.5.758 → 0.5.760
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/Gemfile +2 -2
- data/lib/pwn/ai/agent/loop.rb +102 -32
- data/lib/pwn/banner.rb +157 -38
- data/lib/pwn/plugins/repl/ai_console.rb +63 -9
- data/lib/pwn/plugins/repl/ai_swarm.rb +5 -0
- data/lib/pwn/plugins/repl.rb +19 -3
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +14 -17
- data/spec/lib/pwn/banner_spec.rb +96 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +114 -0
- data/third_party/pwn_rdoc.jsonl +18 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2115a19977c0539855a44ecea57a2fb74bc48ae80ff3a713d2c08b95bdb04e55
|
|
4
|
+
data.tar.gz: 3c237b68883205d53dee7911e18be1e5b5454184f27c7d093e655e75611f5184
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01c9e98a32745f26186d2f664d62f7d1801eea05a56ffe149ea20b84aaef383d5d46d909e5be97247454cedad4781c3ce393783dcc64707a07bbd818a37872c5
|
|
7
|
+
data.tar.gz: 562ea7604c6e23cbe128864aca11338e9c60bd7d83bd5e54692c956f136f29f036e1fb7b63c6cdb9f0defd3168c616cfff5c9194f169b9df209094952e0ae66b
|
data/Gemfile
CHANGED
|
@@ -49,7 +49,7 @@ gem 'jwt', '3.3.0'
|
|
|
49
49
|
gem 'libusb', '0.8.0'
|
|
50
50
|
gem 'luhn', '3.0.0'
|
|
51
51
|
gem 'mail', '2.9.1'
|
|
52
|
-
gem 'mcp', '1.
|
|
52
|
+
gem 'mcp', '1.7.0'
|
|
53
53
|
gem 'meshtastic', '0.0.186'
|
|
54
54
|
gem 'metasm', '1.0.6'
|
|
55
55
|
gem 'mongo', '2.26.0'
|
|
@@ -68,7 +68,7 @@ gem 'ostruct', '0.6.3'
|
|
|
68
68
|
gem 'packetfu', '2.0.0'
|
|
69
69
|
gem 'packetgen', '4.1.1'
|
|
70
70
|
gem 'pdf-reader', '2.16.0'
|
|
71
|
-
gem 'pg', '1.
|
|
71
|
+
gem 'pg', '1.7.0'
|
|
72
72
|
gem 'pry', '0.16.0'
|
|
73
73
|
gem 'pry-doc', '1.7.0'
|
|
74
74
|
gem 'rake', '13.4.2'
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -59,8 +59,78 @@ module PWN
|
|
|
59
59
|
# (:escalation_persona) — the local model still produces the final
|
|
60
60
|
# answer so Learning/Metrics stay attributed to :ollama.
|
|
61
61
|
module Loop
|
|
62
|
+
# Provider calls run on a child. Thread#raise does not wake a native read,
|
|
63
|
+
# so the owner waits in Ruby and leaves within one poll on cancel.
|
|
64
|
+
class ProviderWindow
|
|
65
|
+
def request_model_cancel
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@cancel_requested = true
|
|
68
|
+
if @phase == :model && !@model_signal_sent
|
|
69
|
+
@model_signal_sent = true
|
|
70
|
+
@owner.raise(Steering::ModelCancelled)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def model(messages:, &block)
|
|
76
|
+
result = nil
|
|
77
|
+
worker = nil
|
|
78
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :never) do
|
|
79
|
+
checkpoint(messages: messages, phase: :model)
|
|
80
|
+
raise Steering::ModelCancelled if cancel_requested?
|
|
81
|
+
|
|
82
|
+
worker = provider_thread(&block)
|
|
83
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :immediate) do
|
|
84
|
+
loop do
|
|
85
|
+
raise Steering::ModelCancelled if cancel_requested?
|
|
86
|
+
|
|
87
|
+
break if worker.join(0.05)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
result = worker.value
|
|
91
|
+
ensure
|
|
92
|
+
abandon_provider(worker)
|
|
93
|
+
@mutex.synchronize { @phase = :boundary }
|
|
94
|
+
begin
|
|
95
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :immediate) { nil }
|
|
96
|
+
rescue Steering::ModelCancelled
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
result
|
|
101
|
+
rescue Steering::ModelCancelled
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def cancel_requested?
|
|
108
|
+
@mutex.synchronize { @cancel_requested }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def provider_thread(&block)
|
|
112
|
+
parent = Thread.current
|
|
113
|
+
locals = parent.keys.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent[key]] }
|
|
114
|
+
variables = parent.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent.thread_variable_get(key)] }
|
|
115
|
+
Thread.new do
|
|
116
|
+
Thread.current.report_on_exception = false
|
|
117
|
+
locals.each { |key, value| Thread.current[key] = value }
|
|
118
|
+
variables.each { |key, value| Thread.current.thread_variable_set(key, value) }
|
|
119
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :never, &block)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def abandon_provider(worker)
|
|
124
|
+
return unless worker&.alive?
|
|
125
|
+
|
|
126
|
+
worker.raise(Steering::ModelCancelled)
|
|
127
|
+
worker.join(0.2)
|
|
128
|
+
worker.kill if worker.alive?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
62
132
|
# Request-owned input broker; only the provider window is interruptible.
|
|
63
|
-
class Steering
|
|
133
|
+
class Steering < ProviderWindow
|
|
64
134
|
INPUT_LOCK = Mutex.new
|
|
65
135
|
INPUT_OWNERS = {} # rubocop:disable Style/MutableConstant -- guarded by INPUT_LOCK
|
|
66
136
|
|
|
@@ -80,12 +150,14 @@ module PWN
|
|
|
80
150
|
attr_reader :reader
|
|
81
151
|
|
|
82
152
|
def initialize(input:, output:)
|
|
153
|
+
super()
|
|
83
154
|
@input = input
|
|
84
155
|
@output = output
|
|
85
156
|
@owner = Thread.current
|
|
86
157
|
@mutex = Mutex.new
|
|
87
158
|
@pending = []
|
|
88
159
|
@phase = :boundary
|
|
160
|
+
@cancel_requested = false
|
|
89
161
|
end
|
|
90
162
|
|
|
91
163
|
def submit(text)
|
|
@@ -127,23 +199,6 @@ module PWN
|
|
|
127
199
|
raise Restart.new(messages, instructions)
|
|
128
200
|
end
|
|
129
201
|
|
|
130
|
-
def model(messages:, &block)
|
|
131
|
-
# Drain an in-flight signal inside this rescue, never in a tool.
|
|
132
|
-
Thread.handle_interrupt(ModelCancelled => :never) do
|
|
133
|
-
checkpoint(messages: messages, phase: :model)
|
|
134
|
-
Thread.handle_interrupt(ModelCancelled => :immediate, &block)
|
|
135
|
-
ensure
|
|
136
|
-
@mutex.synchronize { @phase = :boundary }
|
|
137
|
-
begin
|
|
138
|
-
Thread.handle_interrupt(ModelCancelled => :immediate) { nil }
|
|
139
|
-
rescue ModelCancelled
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
rescue ModelCancelled
|
|
144
|
-
nil
|
|
145
|
-
end
|
|
146
|
-
|
|
147
202
|
def with_reader
|
|
148
203
|
require 'io/console'
|
|
149
204
|
input_id = @input.fileno
|
|
@@ -1923,12 +1978,16 @@ module PWN
|
|
|
1923
1978
|
|
|
1924
1979
|
content = msg[:content]
|
|
1925
1980
|
tool_calls = Array(msg[:tool_calls])
|
|
1981
|
+
promoted = false
|
|
1926
1982
|
# Local/thinking models (Ollama Qwen3, DeepSeek-R1, etc.) sometimes
|
|
1927
1983
|
# return only :thinking with empty :content and no tool_calls. Promote
|
|
1928
1984
|
# thinking so the agent does not print a blank final answer.
|
|
1929
1985
|
if content.to_s.strip.empty? && tool_calls.empty?
|
|
1930
1986
|
thinking = msg[:thinking].to_s
|
|
1931
|
-
|
|
1987
|
+
unless thinking.strip.empty?
|
|
1988
|
+
content = thinking
|
|
1989
|
+
promoted = true
|
|
1990
|
+
end
|
|
1932
1991
|
end
|
|
1933
1992
|
|
|
1934
1993
|
out = {
|
|
@@ -1953,6 +2012,7 @@ module PWN
|
|
|
1953
2012
|
out[:_native_content] = msg[:_native_content] if msg[:_native_content]
|
|
1954
2013
|
thinking = msg[:thinking] || msg[:reasoning_content]
|
|
1955
2014
|
out[:thinking] = thinking if thinking.to_s.strip != ''
|
|
2015
|
+
out[:_thinking_promoted] = true if promoted
|
|
1956
2016
|
# Local/abliterated models sometimes emit shell(...) as plain content
|
|
1957
2017
|
# with empty tool_calls. Coerce registered call-shaped text into
|
|
1958
2018
|
# structured tool_calls so Loop dispatches instead of FINAL-answering.
|
|
@@ -2297,6 +2357,17 @@ module PWN
|
|
|
2297
2357
|
nil
|
|
2298
2358
|
end
|
|
2299
2359
|
|
|
2360
|
+
private_class_method def self.emit_thinking!(opts = {})
|
|
2361
|
+
text = opts[:thinking].to_s.strip
|
|
2362
|
+
Thread.current[:pwn_last_thinking] = text
|
|
2363
|
+
Thread.current[:pwn_thinking_promoted] = opts[:promoted] ? true : false
|
|
2364
|
+
return if text.empty?
|
|
2365
|
+
|
|
2366
|
+
opts[:on_tool]&.call('thinking', text, '')
|
|
2367
|
+
rescue StandardError
|
|
2368
|
+
nil
|
|
2369
|
+
end
|
|
2370
|
+
|
|
2300
2371
|
# Pre-dispatch: one high-level brief for the whole upcoming tool
|
|
2301
2372
|
# collection (pwn-ai → task is one-to-many with pwn-ai → <tool>).
|
|
2302
2373
|
# Prefer opts[:tools] = [{name:, args:}, ...]; falls back to single name.
|
|
@@ -2304,18 +2375,13 @@ module PWN
|
|
|
2304
2375
|
state = opts[:state]
|
|
2305
2376
|
return nil unless state && defined?(TaskSummarizer) && TaskSummarizer.enabled?
|
|
2306
2377
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
tools: opts[:tools]
|
|
2315
|
-
)
|
|
2316
|
-
else
|
|
2317
|
-
thinking
|
|
2318
|
-
end
|
|
2378
|
+
line = TaskSummarizer.about_to(
|
|
2379
|
+
name: opts[:name],
|
|
2380
|
+
args: opts[:args],
|
|
2381
|
+
state: state,
|
|
2382
|
+
request: opts[:request],
|
|
2383
|
+
tools: opts[:tools]
|
|
2384
|
+
)
|
|
2319
2385
|
# about_to returns nil when the brief is a duplicate of the last one
|
|
2320
2386
|
emit_task_summary(line: line, on_tool: opts[:on_tool]) if line
|
|
2321
2387
|
line
|
|
@@ -3409,6 +3475,11 @@ module PWN
|
|
|
3409
3475
|
|
|
3410
3476
|
calls = Array(msg[:tool_calls])
|
|
3411
3477
|
text = msg[:content].to_s
|
|
3478
|
+
emit_thinking!(
|
|
3479
|
+
thinking: msg[:thinking] || msg[:reasoning_content],
|
|
3480
|
+
promoted: msg[:_thinking_promoted],
|
|
3481
|
+
on_tool: on_tool
|
|
3482
|
+
)
|
|
3412
3483
|
|
|
3413
3484
|
# Belt-and-suspenders: plain-text shell(...) / tool forms from local
|
|
3414
3485
|
# models under weak TEMPLATE {{ .Prompt }} become real tool_calls.
|
|
@@ -3525,7 +3596,6 @@ module PWN
|
|
|
3525
3596
|
}
|
|
3526
3597
|
end,
|
|
3527
3598
|
request: request,
|
|
3528
|
-
thinking: msg[:thinking] || msg[:reasoning_content],
|
|
3529
3599
|
on_tool: on_tool
|
|
3530
3600
|
)
|
|
3531
3601
|
|
data/lib/pwn/banner.rb
CHANGED
|
@@ -187,6 +187,59 @@ module PWN
|
|
|
187
187
|
end
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
+
# Caller-owned simulation: yields on the UI thread, never reads input or
|
|
191
|
+
# accumulates a replay. The caller supplies the clock and normalized keys.
|
|
192
|
+
class MiniGame
|
|
193
|
+
def initialize(name:, width:, height:, seed: MINI_SEED)
|
|
194
|
+
@name = name.to_s
|
|
195
|
+
raise ArgumentError, "Unknown mini-banner: #{name}" unless Banner.mini_names.map(&:to_s).include?(@name)
|
|
196
|
+
|
|
197
|
+
limit = @name == 'falling_blocks' ? 64 : MINI_WIDTH
|
|
198
|
+
@width = width.clamp(5, limit)
|
|
199
|
+
@height = height.clamp(5, limit)
|
|
200
|
+
@keys = []
|
|
201
|
+
@simulation = Fiber.new do
|
|
202
|
+
options = { frames: self, width: @width, height: @name == 'falling_blocks' ? @height * 2 : @height,
|
|
203
|
+
rng: Random.new(seed), player: self }
|
|
204
|
+
Banner.send("mini_simulate_#{@name}", options)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def key(key)
|
|
209
|
+
@keys << key if @keys.length < 16
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def controls
|
|
213
|
+
keys = @keys
|
|
214
|
+
@keys = []
|
|
215
|
+
keys
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def step
|
|
219
|
+
@simulation.resume
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def cells(frame)
|
|
223
|
+
rows = frame[:rows].drop(1)
|
|
224
|
+
rows = Banner.send(:mini_block_board, board: rows) if @name == 'falling_blocks'
|
|
225
|
+
rows = Banner.send(:mini_quarter_board, board: rows, name: @name) if @name == 'snake'
|
|
226
|
+
rows.map do |row|
|
|
227
|
+
row.chars.map do |pixel|
|
|
228
|
+
glyph, foreground, background = MINI_PIXELS.fetch(pixel)
|
|
229
|
+
{ glyph: glyph.dup, foreground: foreground, background: background || :black }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def <<(frame)
|
|
235
|
+
Fiber.yield(frame)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def length
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
190
243
|
# Six immutable, compact replays bound memory even when callers resize/reseed.
|
|
191
244
|
private_class_method def self.mini_replay(opts = {})
|
|
192
245
|
key = [opts[:name].to_s.dup.freeze, opts[:width], opts[:height], opts[:seed]].freeze
|
|
@@ -347,56 +400,99 @@ module PWN
|
|
|
347
400
|
bag = (0...MINI_SHAPES.length).to_a.shuffle(random: rng) if bag.empty?
|
|
348
401
|
type = bag.shift
|
|
349
402
|
shape = MINI_SHAPES[type]
|
|
350
|
-
landing = mini_landing(board: board, shape: shape, rng: rng)
|
|
403
|
+
landing = opts[:player] ? {} : mini_landing(board: board, shape: shape, rng: rng)
|
|
351
404
|
x = (width - shape.map(&:first).max - 1) / 2
|
|
352
405
|
unless landing && mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
353
406
|
mini_reset_board(frames: frames, board: board)
|
|
354
407
|
board = Array.new(height) { ' ' * width }
|
|
355
408
|
next
|
|
356
409
|
end
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
shape =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
if opts[:player]
|
|
411
|
+
shape, x, y = mini_play_piece(board: board, type: type, frames: frames, player: opts[:player])
|
|
412
|
+
landing = { x: x, y: y }
|
|
413
|
+
else
|
|
414
|
+
shape, x = mini_block_plan(board: board, shape: shape, x: x, landing: landing, frames: frames, type: type)
|
|
415
|
+
unless shape
|
|
416
|
+
mini_reset_board(frames: frames, board: board)
|
|
417
|
+
board = Array.new(height) { ' ' * width }
|
|
418
|
+
next
|
|
366
419
|
end
|
|
367
420
|
end
|
|
368
|
-
until x == landing[:x] || blocked
|
|
369
|
-
x += landing[:x] <=> x
|
|
370
|
-
blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
371
|
-
plan << [shape, x, 0] unless blocked
|
|
372
|
-
end
|
|
373
|
-
if blocked
|
|
374
|
-
mini_reset_board(frames: frames, board: board)
|
|
375
|
-
board = Array.new(height) { ' ' * width }
|
|
376
|
-
next
|
|
377
|
-
end
|
|
378
|
-
(1..landing[:y]).each { |y| plan << [shape, x, y] }
|
|
379
421
|
color = %w[c y m b r g g][type]
|
|
380
|
-
plan.each do |piece, px, py|
|
|
381
|
-
picture = board.map(&:dup)
|
|
382
|
-
active = piece.map { |dx, dy| [px + dx, py + dy] }
|
|
383
|
-
active.each { |ax, ay| picture[ay][ax] = color }
|
|
384
|
-
mini_emit(frames: frames, board: picture,
|
|
385
|
-
state: { active: active, stack: board.map(&:dup), type: type })
|
|
386
|
-
end
|
|
387
422
|
shape.each { |dx, dy| board[landing[:y] + dy][x + dx] = color }
|
|
388
423
|
mini_emit(frames: frames, board: board, event: :lock, ticks: 2)
|
|
389
|
-
full = board.each_index.reject { |
|
|
424
|
+
full = board.each_index.reject { |row| board[row].include?(' ') }
|
|
390
425
|
next if full.empty?
|
|
391
426
|
|
|
392
427
|
flash = board.map(&:dup)
|
|
393
|
-
full.each { |
|
|
428
|
+
full.each { |row| flash[row] = 'w' * width }
|
|
394
429
|
mini_emit(frames: frames, board: flash, event: :clear, state: { lines: full }, ticks: 3)
|
|
395
|
-
board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row,
|
|
430
|
+
board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row, index| row unless full.include?(index) }
|
|
396
431
|
mini_emit(frames: frames, board: board, event: :collapse, ticks: 2)
|
|
397
432
|
end
|
|
398
433
|
end
|
|
399
434
|
|
|
435
|
+
private_class_method def self.mini_block_plan(opts = {})
|
|
436
|
+
board, shape, x, landing, frames, type = opts.values_at(:board, :shape, :x, :landing, :frames, :type)
|
|
437
|
+
rotations = mini_rotations(shape: shape)
|
|
438
|
+
plan = [[shape, x, 0]]
|
|
439
|
+
blocked = false
|
|
440
|
+
(1..landing[:rotation]).each do |rotation|
|
|
441
|
+
shape = rotations[rotation]
|
|
442
|
+
if mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
443
|
+
plan << [shape, x, 0]
|
|
444
|
+
else
|
|
445
|
+
blocked = true
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
until x == landing[:x] || blocked
|
|
449
|
+
x += landing[:x] <=> x
|
|
450
|
+
blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
451
|
+
plan << [shape, x, 0] unless blocked
|
|
452
|
+
end
|
|
453
|
+
return if blocked
|
|
454
|
+
|
|
455
|
+
(1..landing[:y]).each { |y| plan << [shape, x, y] }
|
|
456
|
+
color = %w[c y m b r g g][type]
|
|
457
|
+
plan.each do |piece, px, py|
|
|
458
|
+
picture = board.map(&:dup)
|
|
459
|
+
active = piece.map { |dx, dy| [px + dx, py + dy] }
|
|
460
|
+
active.each { |ax, ay| picture[ay][ax] = color }
|
|
461
|
+
mini_emit(frames: frames, board: picture,
|
|
462
|
+
state: { active: active, stack: board.map(&:dup), type: type })
|
|
463
|
+
end
|
|
464
|
+
[shape, x]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private_class_method def self.mini_play_piece(opts = {})
|
|
468
|
+
board, type, frames, player = opts.values_at(:board, :type, :frames, :player)
|
|
469
|
+
shape = MINI_SHAPES[type]
|
|
470
|
+
x = (board.first.length - shape.map(&:first).max - 1) / 2
|
|
471
|
+
y = 0
|
|
472
|
+
tick = 0
|
|
473
|
+
loop do
|
|
474
|
+
active = shape.map { |dx, dy| [x + dx, y + dy] }
|
|
475
|
+
picture = board.map(&:dup)
|
|
476
|
+
active.each { |ax, ay| picture[ay][ax] = %w[c y m b r g g][type] }
|
|
477
|
+
mini_emit(frames: frames, board: picture, state: { active: active, stack: board.map(&:dup), type: type })
|
|
478
|
+
keys = player.controls
|
|
479
|
+
keys.each do |key|
|
|
480
|
+
candidate = key == ' ' ? mini_rotations(shape: shape).fetch(1, shape) : shape
|
|
481
|
+
nx = x + { left: -1, right: 1 }.fetch(key, 0)
|
|
482
|
+
next unless mini_fits?(board: board, shape: candidate, x: nx, y: y)
|
|
483
|
+
|
|
484
|
+
shape = candidate
|
|
485
|
+
x = nx
|
|
486
|
+
end
|
|
487
|
+
tick += 1
|
|
488
|
+
next unless keys.include?(:down) || (tick % 5).zero?
|
|
489
|
+
break unless mini_fits?(board: board, shape: shape, x: x, y: y + 1)
|
|
490
|
+
|
|
491
|
+
y += 1
|
|
492
|
+
end
|
|
493
|
+
[shape, x, y]
|
|
494
|
+
end
|
|
495
|
+
|
|
400
496
|
private_class_method def self.mini_reset_board(opts = {})
|
|
401
497
|
board = opts[:board].map { |row| row.gsub(/[^ ]/, 'w') }
|
|
402
498
|
mini_emit(frames: opts[:frames], board: board, event: :game_over, ticks: 8)
|
|
@@ -442,19 +538,31 @@ module PWN
|
|
|
442
538
|
height = opts[:height]
|
|
443
539
|
rng = opts[:rng]
|
|
444
540
|
spaces = (0...height).flat_map { |y| (0...width).map { |x| [x, y] } }
|
|
541
|
+
player = opts[:player]
|
|
445
542
|
while frames.length < MINI_FRAME_COUNT
|
|
446
543
|
y = rng.rand(height)
|
|
447
544
|
body = [[2, y], [1, y], [0, y]]
|
|
545
|
+
direction = [1, 0]
|
|
448
546
|
food = (spaces - body).sample(random: rng)
|
|
449
547
|
steps = 0
|
|
450
548
|
loop do
|
|
451
549
|
board = Array.new(height) { ' ' * width }
|
|
452
550
|
body.each_with_index { |(x, row), index| board[row][x] = index.zero? ? 'y' : 'g' }
|
|
453
551
|
board[food[1]][food[0]] = 'f' if food
|
|
454
|
-
mini_emit(frames: frames, board: board, ticks: 2, state: { body: body.map(&:dup), food: food&.dup })
|
|
552
|
+
mini_emit(frames: frames, board: board, ticks: player ? 1 : 2, state: { body: body.map(&:dup), food: food&.dup })
|
|
455
553
|
break if frames.length >= MINI_FRAME_COUNT
|
|
456
554
|
|
|
457
|
-
|
|
555
|
+
if player
|
|
556
|
+
turn = player.controls.filter_map { |key| { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[key] }.find do |dx, dy|
|
|
557
|
+
[dx, dy] != direction && [dx, dy] != direction.map(&:-@)
|
|
558
|
+
end
|
|
559
|
+
direction = turn if turn
|
|
560
|
+
target = [body.first[0] + direction[0], body.first[1] + direction[1]]
|
|
561
|
+
occupied = target == food ? body : body.take(body.length - 1)
|
|
562
|
+
target = nil unless spaces.include?(target) && !occupied.include?(target)
|
|
563
|
+
elsif food
|
|
564
|
+
target = mini_snake_route(body: body, food: food, width: width, height: height, rng: rng)
|
|
565
|
+
end
|
|
458
566
|
if !food || !target || steps >= width * height * 4
|
|
459
567
|
mini_reset_board(frames: frames, board: board)
|
|
460
568
|
break
|
|
@@ -477,6 +585,7 @@ module PWN
|
|
|
477
585
|
height = opts[:height]
|
|
478
586
|
rng = opts[:rng]
|
|
479
587
|
paddles = [(height - 1) / 2.0, (height - 1) / 2.0]
|
|
588
|
+
player = opts[:player]
|
|
480
589
|
scores = [0, 0]
|
|
481
590
|
while frames.length < MINI_FRAME_COUNT
|
|
482
591
|
x = (width - 1) / 2.0
|
|
@@ -485,9 +594,14 @@ module PWN
|
|
|
485
594
|
vy = ((rng.rand * 0.4) + 0.1) * (rng.rand(2).zero? ? -1 : 1)
|
|
486
595
|
bias = Array.new(2) { rng.rand(5).zero? ? rng.rand(-2..2) : rng.rand - 0.5 }
|
|
487
596
|
rally = 0
|
|
488
|
-
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: 16)
|
|
597
|
+
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: player ? 1 : 16)
|
|
489
598
|
loop do
|
|
599
|
+
player&.controls&.each do |key|
|
|
600
|
+
paddles[0] = (paddles[0] + { up: -0.5, down: 0.5 }.fetch(key, 0)).clamp(0, height - 1)
|
|
601
|
+
end
|
|
490
602
|
paddles.each_index do |side|
|
|
603
|
+
next if player && side.zero?
|
|
604
|
+
|
|
491
605
|
incoming = side.zero? ? vx.negative? : vx.positive?
|
|
492
606
|
target = incoming ? y - 0.25 + bias[side] : (height - 1) / 2.0
|
|
493
607
|
speed = incoming ? 0.275 : 0.11
|
|
@@ -519,7 +633,7 @@ module PWN
|
|
|
519
633
|
y = ny
|
|
520
634
|
if x.negative? || x > width - 1
|
|
521
635
|
scores[x.negative? ? 1 : 0] += 1
|
|
522
|
-
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: 20)
|
|
636
|
+
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: player ? 1 : 20)
|
|
523
637
|
break
|
|
524
638
|
end
|
|
525
639
|
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: event, rally: rally)
|
|
@@ -594,13 +708,18 @@ module PWN
|
|
|
594
708
|
end
|
|
595
709
|
angle = Math.atan2(mini_delta(from: ship[1], to: target[1], span: height), mini_delta(from: ship[0], to: target[0], span: width))
|
|
596
710
|
turn = mini_delta(from: ship[2], to: angle, span: Math::PI * 2).clamp(-0.1, 0.1)
|
|
711
|
+
keys = opts[:player]&.controls
|
|
712
|
+
turn = keys.sum { |key| { left: -0.2, right: 0.2 }.fetch(key, 0) } if keys
|
|
597
713
|
ship[2] = (ship[2] + turn) % (Math::PI * 2)
|
|
598
|
-
|
|
714
|
+
power = tick % 80 < 24 ? 1 : 0
|
|
715
|
+
power = keys.sum { |key| { up: 1, down: -1 }.fetch(key, 0) }.clamp(-1, 1) if keys
|
|
716
|
+
thrust = !power.zero?
|
|
599
717
|
[Math.cos(ship[2]), Math.sin(ship[2])].each_with_index do |direction, axis|
|
|
600
|
-
velocity[axis] = ((velocity[axis] * 0.99) + (
|
|
718
|
+
velocity[axis] = ((velocity[axis] * 0.99) + (direction * 0.006 * power)).clamp(-0.15, 0.15)
|
|
601
719
|
ship[axis] = (ship[axis] + velocity[axis]) % (axis.zero? ? width : height)
|
|
602
720
|
end
|
|
603
|
-
|
|
721
|
+
fire = keys ? keys.include?(' ') : (tick % 12).zero?
|
|
722
|
+
shots << [ship[0], ship[1], Math.cos(ship[2]) * 0.36, Math.sin(ship[2]) * 0.36, 24] if fire
|
|
604
723
|
shots.each do |shot|
|
|
605
724
|
shot[0] = (shot[0] + shot[2]) % width
|
|
606
725
|
shot[1] = (shot[1] + shot[3]) % height
|
|
@@ -86,6 +86,7 @@ module PWN
|
|
|
86
86
|
|
|
87
87
|
def stop
|
|
88
88
|
@stopping = true
|
|
89
|
+
request_model_cancel
|
|
89
90
|
submit('Stop the active request.')
|
|
90
91
|
end
|
|
91
92
|
|
|
@@ -266,7 +267,7 @@ module PWN
|
|
|
266
267
|
|
|
267
268
|
# Own the terminal, event pump and the lifetime of exactly one request.
|
|
268
269
|
class Console # rubocop:disable Metrics/ClassLength -- cohesive single-owner terminal state machine
|
|
269
|
-
COLORS = { operator: :operator, task: :task, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
|
|
270
|
+
COLORS = { operator: :operator, task: :task, thinking: :notice, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
|
|
270
271
|
PALETTE = %w[cyan green yellow red white blue magenta black].freeze
|
|
271
272
|
|
|
272
273
|
def initialize(pry:, input:, curses:, getch:)
|
|
@@ -480,6 +481,8 @@ module PWN
|
|
|
480
481
|
on_tool = lambda do |name, args, result|
|
|
481
482
|
if name.to_s == 'task'
|
|
482
483
|
@events << [:task, args.to_s]
|
|
484
|
+
elsif name.to_s == 'thinking'
|
|
485
|
+
@events << [:thinking, args.to_s]
|
|
483
486
|
else
|
|
484
487
|
@events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
|
|
485
488
|
@events << [:result, result.to_s]
|
|
@@ -503,7 +506,8 @@ module PWN
|
|
|
503
506
|
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
504
507
|
on_tool: on_tool, steering: control, debug: @pry.config.pwn_ai_debug, debug_tee: @output
|
|
505
508
|
)
|
|
506
|
-
|
|
509
|
+
promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
|
|
510
|
+
@events << [:assistant, final.to_s] unless promoted
|
|
507
511
|
end
|
|
508
512
|
rescue Control::Stopped => e
|
|
509
513
|
@events << [:warning, e.message]
|
|
@@ -521,13 +525,12 @@ module PWN
|
|
|
521
525
|
|
|
522
526
|
def cancel
|
|
523
527
|
if busy?
|
|
524
|
-
|
|
525
|
-
|
|
528
|
+
first = !@cancelling
|
|
526
529
|
@cancelling = true
|
|
527
530
|
@control&.stop
|
|
528
531
|
@swarm&.cancel_all
|
|
529
532
|
@tool_input.close unless @tool_input.closed?
|
|
530
|
-
add(:warning, 'Cancellation requested. Waiting for
|
|
533
|
+
add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
|
|
531
534
|
else
|
|
532
535
|
@editor.replace('')
|
|
533
536
|
end
|
|
@@ -678,6 +681,7 @@ module PWN
|
|
|
678
681
|
def handle(key)
|
|
679
682
|
return if key.nil?
|
|
680
683
|
return handle_system_role(key) if @role_editor && (!@too_small || ["\e", "\u0003", "\u0004", :eof].include?(key))
|
|
684
|
+
return cancel if key == "\u0003" && busy?
|
|
681
685
|
|
|
682
686
|
return if @too_small && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
683
687
|
return cancel if @too_small && key == "\u0003" && !@model_prompt
|
|
@@ -711,7 +715,10 @@ module PWN
|
|
|
711
715
|
|
|
712
716
|
if ["\u0018", "\u0014"].include?(key)
|
|
713
717
|
@menu = nil
|
|
714
|
-
|
|
718
|
+
panes = @header_pane_height ? %i[mission session animation] : %i[mission session]
|
|
719
|
+
@focus = panes[(panes.index(@focus).to_i + 1) % panes.length]
|
|
720
|
+
@banner_game&.controls
|
|
721
|
+
@banner_game_tick = nil
|
|
715
722
|
return
|
|
716
723
|
end
|
|
717
724
|
|
|
@@ -723,6 +730,8 @@ module PWN
|
|
|
723
730
|
@menu = nil
|
|
724
731
|
return
|
|
725
732
|
end
|
|
733
|
+
return handle_animation(key) if @focus == :animation && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
734
|
+
|
|
726
735
|
case key
|
|
727
736
|
when :eof, "\u0004"
|
|
728
737
|
@leaving = true
|
|
@@ -791,6 +800,24 @@ module PWN
|
|
|
791
800
|
end
|
|
792
801
|
end
|
|
793
802
|
|
|
803
|
+
def handle_animation(key)
|
|
804
|
+
if key == :paste_start
|
|
805
|
+
@focus = :mission
|
|
806
|
+
@paste = true
|
|
807
|
+
@banner_game&.controls
|
|
808
|
+
elsif [:up, :down, :left, :right, ' '].include?(key)
|
|
809
|
+
@banner_game&.key(key)
|
|
810
|
+
elsif ["\r", "\n", "\e", :newline].include?(key)
|
|
811
|
+
@focus = :mission
|
|
812
|
+
@banner_game&.controls
|
|
813
|
+
elsif key.is_a?(String) && key.ord >= 32
|
|
814
|
+
@focus = :mission
|
|
815
|
+
@banner_game&.controls
|
|
816
|
+
@editor.insert(key)
|
|
817
|
+
refresh_completion
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
794
821
|
def open_system_role
|
|
795
822
|
return add(:warning, 'System role cannot change while a request or swarm job is running.') if busy?
|
|
796
823
|
|
|
@@ -1198,6 +1225,7 @@ module PWN
|
|
|
1198
1225
|
end
|
|
1199
1226
|
cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
|
|
1200
1227
|
frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
|
|
1228
|
+
return playable_banner(size, now) if cells && @focus == :animation
|
|
1201
1229
|
return PWN::Banner.mini_cells(name: @banner_name, frame: frame, width: size, height: size, seed: @banner_seed, branding: false) if cells
|
|
1202
1230
|
|
|
1203
1231
|
art = PWN::Banner.mini_frame(name: @banner_name, frame: frame, width: size, height: size, branding: false)
|
|
@@ -1207,6 +1235,21 @@ module PWN
|
|
|
1207
1235
|
canvas
|
|
1208
1236
|
end
|
|
1209
1237
|
|
|
1238
|
+
def playable_banner(size, now)
|
|
1239
|
+
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1240
|
+
if @banner_game_key != key
|
|
1241
|
+
@banner_game_key = key
|
|
1242
|
+
@banner_game = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed)
|
|
1243
|
+
@banner_game_tick = nil
|
|
1244
|
+
end
|
|
1245
|
+
tick = (now / @banner_frame_seconds).floor
|
|
1246
|
+
if @banner_game_tick != tick
|
|
1247
|
+
@banner_game_cells = @banner_game.cells(@banner_game.step)
|
|
1248
|
+
@banner_game_tick = tick
|
|
1249
|
+
end
|
|
1250
|
+
@banner_game_cells
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1210
1253
|
def draw_banner(size)
|
|
1211
1254
|
cells = banner_frame(size, cells: true)
|
|
1212
1255
|
top = [(size - cells.length) / 2, 0].max
|
|
@@ -1264,6 +1307,11 @@ module PWN
|
|
|
1264
1307
|
|
|
1265
1308
|
def footer_text
|
|
1266
1309
|
text = '/ menu · ^C=cancel · ^D=back · ^L=clear · ^T=toggle pane · ^S=swarm · ^R=search ^O=operations · ↑↓ history · Shift+Enter=newline · Enter=send'
|
|
1310
|
+
if @focus == :animation
|
|
1311
|
+
controls = { pong: '↑↓ paddle', snake: '↑↓←→ steer', falling_blocks: '←→ move · ↓ faster · Space=rotate',
|
|
1312
|
+
asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire' }.fetch(@banner_name, '↑↓←→ play')
|
|
1313
|
+
return text.sub('↑↓ history', controls).sub('Shift+Enter=newline · Enter=send', 'Enter/Esc=draft')
|
|
1314
|
+
end
|
|
1267
1315
|
@focus == :session ? text.sub('↑↓ history', '↑↓ scroll · HOME · PGUP · PGDN · END') : text
|
|
1268
1316
|
end
|
|
1269
1317
|
|
|
@@ -1292,6 +1340,7 @@ module PWN
|
|
|
1292
1340
|
visible.each_with_index { |spans, index| put_spans(2 + index, @header_text_column, spans) }
|
|
1293
1341
|
if @header_pane_height
|
|
1294
1342
|
box(0, 0, height, height, '')
|
|
1343
|
+
mark_active(0, 0, height, 'PLAY') if @focus == :animation
|
|
1295
1344
|
draw_banner(height - 2)
|
|
1296
1345
|
end
|
|
1297
1346
|
height
|
|
@@ -1335,6 +1384,8 @@ module PWN
|
|
|
1335
1384
|
end
|
|
1336
1385
|
engine = PWN::Env.dig(:ai, :active).to_s
|
|
1337
1386
|
model = REPL.pwn_ai_engine_model(engine: engine)
|
|
1387
|
+
header_layout(engine, model)
|
|
1388
|
+
@focus = :mission if @focus == :animation && !@header_pane_height
|
|
1338
1389
|
footer, compose_height, footer_height = layout_chrome
|
|
1339
1390
|
@compose_height = compose_height
|
|
1340
1391
|
@footer_height = footer_height
|
|
@@ -1356,9 +1407,9 @@ module PWN
|
|
|
1356
1407
|
rows.slice(start, @page_size).to_a.each_with_index { |(color, row), index| put(header_height + 1 + index, 2, row, color) }
|
|
1357
1408
|
draw_sidebar(header_height, pane_width + 1, timeline_height) if sidebar.positive?
|
|
1358
1409
|
compose_y = @height - compose_height - footer_height
|
|
1359
|
-
mission_title = @focus == :
|
|
1410
|
+
mission_title = @focus == :mission ? 'MISSION CONTROL · active' : 'MISSION CONTROL'
|
|
1360
1411
|
box(compose_y, 0, compose_height, @width - 1, mission_title)
|
|
1361
|
-
mark_active(compose_y, 0, @width - 1, mission_title) if @focus
|
|
1412
|
+
mark_active(compose_y, 0, @width - 1, mission_title) if @focus == :mission
|
|
1362
1413
|
draw_composer(compose_y, height: compose_height)
|
|
1363
1414
|
footer_top = @height - footer_height
|
|
1364
1415
|
footer.first(footer_height).each_with_index { |line, index| put(footer_top + index, 1, line, tone(:footer)) }
|
|
@@ -1367,7 +1418,9 @@ module PWN
|
|
|
1367
1418
|
draw_details(engine, model) if @details
|
|
1368
1419
|
draw_swarm if @workspace
|
|
1369
1420
|
draw_system_role if @role_editor
|
|
1370
|
-
if @focus == :
|
|
1421
|
+
if @focus == :animation && !(@details || @workspace || @role_editor || @model_prompt)
|
|
1422
|
+
@screen.setpos(1, 1)
|
|
1423
|
+
elsif @focus == :session && !(@details || @workspace || @role_editor)
|
|
1371
1424
|
@screen.setpos(header_height + 1, 2)
|
|
1372
1425
|
elsif @role_editor || !(@details || @workspace)
|
|
1373
1426
|
@screen.setpos(*@cursor_position)
|
|
@@ -1652,6 +1705,7 @@ module PWN
|
|
|
1652
1705
|
puts "USAGE:
|
|
1653
1706
|
# Ctrl+L clears only the session pane, including while busy.
|
|
1654
1707
|
# Ctrl+T toggles the active pane between SESSION and MISSION CONTROL. Ctrl+S opens swarm.
|
|
1708
|
+
# Thinking from a model that returns it is a THINKING row in the session pane, above the answer.
|
|
1655
1709
|
# Up/Down select in menus, recall ~/.pwn/pwn_history in the mission pane, and scroll the session pane when it is active.
|
|
1656
1710
|
# Search: Enter accepts without sending; Esc restores the draft.
|
|
1657
1711
|
# Launch the single-owner curses console; non-terminals return unavailable.
|
|
@@ -15,6 +15,7 @@ module PWN
|
|
|
15
15
|
|
|
16
16
|
def stop
|
|
17
17
|
@stopping = true
|
|
18
|
+
request_model_cancel
|
|
18
19
|
submit('Stop this swarm job.')
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -279,6 +280,10 @@ module PWN
|
|
|
279
280
|
|
|
280
281
|
def tool_callback(job, opts)
|
|
281
282
|
lambda do |name, args, result|
|
|
283
|
+
if name.to_s == 'thinking'
|
|
284
|
+
opts[:on_event]&.call(:thinking, args.to_s)
|
|
285
|
+
return
|
|
286
|
+
end
|
|
282
287
|
update(job, job[:state], last_tool: name)
|
|
283
288
|
opts[:on_event]&.call(:tool, "#{job[:agent]} [#{job[:id]}] #{name}\n#{args.inspect}")
|
|
284
289
|
opts[:on_event]&.call(:result, result.to_s)
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -561,6 +561,19 @@ module PWN
|
|
|
561
561
|
mirror.call("#{header}\n#{body_out}")
|
|
562
562
|
next
|
|
563
563
|
end
|
|
564
|
+
if name.to_s == 'thinking'
|
|
565
|
+
body = args.is_a?(String) ? args.to_s : args.inspect
|
|
566
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
567
|
+
header = "[ #{timestamp} → pwn-ai → thinking ]"
|
|
568
|
+
puts "\e[36m#{header}\e[0m"
|
|
569
|
+
body_out = +''
|
|
570
|
+
body.to_s.each_line do |ln|
|
|
571
|
+
puts "\e[36m #{ln.rstrip}\e[0m"
|
|
572
|
+
body_out << " #{ln.rstrip}\n"
|
|
573
|
+
end
|
|
574
|
+
mirror.call("#{header}\n#{body_out}")
|
|
575
|
+
next
|
|
576
|
+
end
|
|
564
577
|
|
|
565
578
|
argv = args.is_a?(String) ? args.to_s : args.inspect
|
|
566
579
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
@@ -592,9 +605,12 @@ module PWN
|
|
|
592
605
|
debug_tee: $stdout
|
|
593
606
|
)
|
|
594
607
|
$stdout.flush
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
608
|
+
promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
|
|
609
|
+
unless promoted
|
|
610
|
+
puts "\n\e[32m#{final}\e[0m\n\n"
|
|
611
|
+
$stdout.flush
|
|
612
|
+
PWN::Plugins::Log.mirror_tui!(msg: "\n#{final}\n\n") if pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
|
|
613
|
+
end
|
|
598
614
|
if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
|
|
599
615
|
PWN::Plugins::Log.progress(
|
|
600
616
|
msg: "session=#{sess_id}",
|
data/lib/pwn/version.rb
CHANGED
|
@@ -228,7 +228,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
228
228
|
calls = []
|
|
229
229
|
owner = Thread.current
|
|
230
230
|
allow(described_class).to receive(:call_engine) do |opts|
|
|
231
|
-
expect(Thread.current).
|
|
231
|
+
expect(Thread.current).not_to eq(owner)
|
|
232
232
|
calls << Marshal.load(Marshal.dump(opts[:messages]))
|
|
233
233
|
if calls.length == 1
|
|
234
234
|
entered << true
|
|
@@ -2123,36 +2123,33 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
2123
2123
|
expect(src).to include('byteslice(0, 2_048)')
|
|
2124
2124
|
end
|
|
2125
2125
|
|
|
2126
|
-
describe 'thinking on the
|
|
2127
|
-
it '
|
|
2126
|
+
describe 'thinking on the session pane' do
|
|
2127
|
+
it 'keeps the task brief and emits thinking separately' do
|
|
2128
2128
|
allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(true)
|
|
2129
2129
|
allow(PWN::AI::Agent::TaskSummarizer).to receive(:about_to).and_return('task 1/3: scan')
|
|
2130
2130
|
seen = []
|
|
2131
2131
|
described_class.send(
|
|
2132
|
-
:
|
|
2133
|
-
state: { plan: ['scan'] },
|
|
2134
|
-
tools: [{ name: 'shell' }],
|
|
2135
|
-
request: 'scan the host',
|
|
2132
|
+
:emit_thinking!,
|
|
2136
2133
|
thinking: 'Need to fingerprint the service before scanning.',
|
|
2137
2134
|
on_tool: ->(name, args, _res) { seen << [name, args] }
|
|
2138
2135
|
)
|
|
2139
|
-
expect(seen).to eq([['task', 'Need to fingerprint the service before scanning.']])
|
|
2140
|
-
expect(PWN::AI::Agent::TaskSummarizer).not_to have_received(:about_to)
|
|
2141
|
-
end
|
|
2142
|
-
|
|
2143
|
-
it 'falls back to the TaskSummarizer brief when thinking is blank' do
|
|
2144
|
-
allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(true)
|
|
2145
|
-
allow(PWN::AI::Agent::TaskSummarizer).to receive(:about_to).and_return('task 1/3: scan')
|
|
2146
|
-
seen = []
|
|
2147
2136
|
described_class.send(
|
|
2148
2137
|
:task_summary_about_to!,
|
|
2149
2138
|
state: { plan: ['scan'] },
|
|
2150
2139
|
tools: [{ name: 'shell' }],
|
|
2151
2140
|
request: 'scan the host',
|
|
2152
|
-
thinking: ' ',
|
|
2153
2141
|
on_tool: ->(name, args, _res) { seen << [name, args] }
|
|
2154
2142
|
)
|
|
2155
|
-
expect(seen).to eq([
|
|
2143
|
+
expect(seen).to eq([
|
|
2144
|
+
['thinking', 'Need to fingerprint the service before scanning.'],
|
|
2145
|
+
['task', 'task 1/3: scan']
|
|
2146
|
+
])
|
|
2147
|
+
end
|
|
2148
|
+
|
|
2149
|
+
it 'does not emit a blank thinking row' do
|
|
2150
|
+
seen = []
|
|
2151
|
+
described_class.send(:emit_thinking!, thinking: ' ', on_tool: ->(name, args, _res) { seen << [name, args] })
|
|
2152
|
+
expect(seen).to eq([])
|
|
2156
2153
|
end
|
|
2157
2154
|
end
|
|
2158
2155
|
|
data/spec/lib/pwn/banner_spec.rb
CHANGED
|
@@ -25,7 +25,103 @@ describe PWN::Banner do
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe PWN::Banner::MiniGame do
|
|
31
|
+
it 'renders bounded, unbranded colored frames without accumulating a replay' do
|
|
32
|
+
PWN::Banner.mini_names.product([5, 8, 16]).each do |name, size|
|
|
33
|
+
game = described_class.new(name: name, width: size, height: size)
|
|
34
|
+
40.times do
|
|
35
|
+
frame = game.step
|
|
36
|
+
cells = game.cells(frame)
|
|
37
|
+
expect(cells.length).to eq(size)
|
|
38
|
+
expect(cells.map(&:length)).to all(eq(size))
|
|
39
|
+
expect(cells.flatten.map { |cell| cell[:glyph] }.join).not_to match(/[PWN]/)
|
|
40
|
+
expect(cells.flatten).to all(include(:foreground, :background))
|
|
41
|
+
end
|
|
42
|
+
expect(game.length).to eq(0)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'lets the player move only the left Pong paddle while the ball and opponent keep playing' do
|
|
47
|
+
game = PWN::Banner::MiniGame.new(name: :pong, width: 8, height: 8, seed: 73)
|
|
48
|
+
first = game.step
|
|
49
|
+
game.key(:up)
|
|
50
|
+
moved = game.step
|
|
51
|
+
expect(moved[:state][:paddles][0]).to be < first[:state][:paddles][0]
|
|
52
|
+
expect(moved[:state][:ball]).not_to eq(first[:state][:ball])
|
|
53
|
+
game.key(:down)
|
|
54
|
+
expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
|
|
55
|
+
game.key(:left)
|
|
56
|
+
expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
|
|
57
|
+
100.times do
|
|
58
|
+
game.key(:up)
|
|
59
|
+
game.step
|
|
60
|
+
end
|
|
61
|
+
expect(game.step[:state][:paddles][0]).to eq(0)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'steers Snake with arrows without reversing into its neck or changing direction twice per tick' do
|
|
65
|
+
game = PWN::Banner::MiniGame.new(name: :snake, width: 16, height: 16, seed: 73)
|
|
66
|
+
first = game.step[:state]
|
|
67
|
+
game.key(:left)
|
|
68
|
+
moved = game.step[:state]
|
|
69
|
+
expect(moved[:body].first).to eq([first[:body][0][0] + 1, first[:body][0][1]])
|
|
70
|
+
turn = moved[:body][0][1].positive? ? :up : :down
|
|
71
|
+
game.key(turn)
|
|
72
|
+
game.key(:left)
|
|
73
|
+
turned = game.step[:state]
|
|
74
|
+
expect(turned[:body].first).to eq([moved[:body][0][0], moved[:body][0][1] + (turn == :up ? -1 : 1)])
|
|
75
|
+
game.key(:left)
|
|
76
|
+
expect(game.step[:state][:body].first[0]).to eq(turned[:body][0][0] - 1)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'moves Tetris sideways, rotates with space and soft drops one row with down' do
|
|
80
|
+
game = PWN::Banner::MiniGame.new(name: :falling_blocks, width: 8, height: 8, seed: 73)
|
|
81
|
+
first = game.step[:state]
|
|
82
|
+
game.key(:left)
|
|
83
|
+
left = game.step[:state]
|
|
84
|
+
expect(left[:active]).to eq(first[:active].map { |x, y| [x - 1, y] })
|
|
85
|
+
game.key(:right)
|
|
86
|
+
expect(game.step[:state][:active]).to eq(first[:active])
|
|
87
|
+
game.key(' ')
|
|
88
|
+
rotated = game.step[:state]
|
|
89
|
+
expect(rotated[:active]).not_to eq(first[:active])
|
|
90
|
+
expect(rotated[:active].map(&:last).min).to eq(0)
|
|
91
|
+
game.key(:down)
|
|
92
|
+
dropped = game.step[:state]
|
|
93
|
+
expect(dropped[:active]).to eq(rotated[:active].map { |x, y| [x, y + 1] })
|
|
94
|
+
expect(dropped[:stack]).to eq(first[:stack])
|
|
95
|
+
events = 150.times.map do
|
|
96
|
+
game.key(:down)
|
|
97
|
+
game.step[:event]
|
|
98
|
+
end
|
|
99
|
+
expect(events).to include(:lock)
|
|
100
|
+
end
|
|
28
101
|
|
|
102
|
+
it 'turns and thrusts Asteroids only on arrows and fires only on space' do
|
|
103
|
+
game = PWN::Banner::MiniGame.new(name: :asteroids, width: 16, height: 16, seed: 73)
|
|
104
|
+
first = game.step[:state]
|
|
105
|
+
idle = game.step[:state]
|
|
106
|
+
expect(idle[:ship]).to eq(first[:ship])
|
|
107
|
+
expect(idle[:shots]).to be_empty
|
|
108
|
+
game.key(:left)
|
|
109
|
+
left = game.step[:state]
|
|
110
|
+
expect(left[:ship][2]).to be_within(0.001).of((first[:ship][2] - 0.2) % (Math::PI * 2))
|
|
111
|
+
game.key(:right)
|
|
112
|
+
expect(game.step[:state][:ship][2]).to be_within(0.001).of(first[:ship][2])
|
|
113
|
+
game.key(:up)
|
|
114
|
+
moved = game.step[:state]
|
|
115
|
+
expect(moved[:ship].take(2)).not_to eq(first[:ship].take(2))
|
|
116
|
+
expect(moved[:thrust]).to be(true)
|
|
117
|
+
game.key(:down)
|
|
118
|
+
expect(game.step[:state][:thrust]).to be(true)
|
|
119
|
+
game.key(' ')
|
|
120
|
+
expect(game.step[:state][:shots].length).to eq(1)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe PWN::Banner do
|
|
29
125
|
def block_colors(rows)
|
|
30
126
|
rows.flat_map do |row|
|
|
31
127
|
[0, 2].map do |bit|
|
|
@@ -258,6 +258,27 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
258
258
|
foreign&.join
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
it 'leaves a provider blocked in native IO without waiting for that read to return' do
|
|
262
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
263
|
+
allow(PWN::AI::Agent::Loop).to receive(:call_engine) do
|
|
264
|
+
Thread.handle_interrupt(Exception => :never) { sleep 8 }
|
|
265
|
+
end
|
|
266
|
+
keys = "what color is a lemon?\n".chars
|
|
267
|
+
cancelled = false
|
|
268
|
+
launch(keys) do
|
|
269
|
+
if !cancelled && @paint.any? { |row| row.include?('Elapsed') }
|
|
270
|
+
cancelled = true
|
|
271
|
+
"\u0003"
|
|
272
|
+
elsif @paint.any? { |row| row.include?('Request cancelled at a safe boundary') }
|
|
273
|
+
"\u0004"
|
|
274
|
+
else
|
|
275
|
+
Thread.pass
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
|
|
280
|
+
end
|
|
281
|
+
|
|
261
282
|
it 'forwards tool input, preserves completed evidence before steering, and locks settings while running' do
|
|
262
283
|
entered = Queue.new
|
|
263
284
|
calls = 0
|
|
@@ -389,6 +410,19 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
389
410
|
expect(colors).to eq(operator_label: 4, operator_body: 5, assistant: 5, task: 2, tool: 1, result: 3)
|
|
390
411
|
end
|
|
391
412
|
|
|
413
|
+
it 'shows model thinking in the session pane even when compact' do
|
|
414
|
+
pry = Pry.new
|
|
415
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
|
|
416
|
+
console.instance_variable_set(:@verbose, false)
|
|
417
|
+
console.add(:thinking, 'The lemon is yellow because of carotenoids.')
|
|
418
|
+
console.add(:assistant, 'Yellow.')
|
|
419
|
+
text = console.timeline_rows(60).map { |_color, row| row }.join("\n")
|
|
420
|
+
expect(text).to include('THINKING')
|
|
421
|
+
expect(text).to include('The lemon is yellow because of carotenoids.')
|
|
422
|
+
expect(text).to include('ASSISTANT')
|
|
423
|
+
expect(text).to include('Yellow.')
|
|
424
|
+
end
|
|
425
|
+
|
|
392
426
|
it 'uses template colors for every unconfigured role without mutating defaults or overrides' do
|
|
393
427
|
defaults = PWN::Config.env_template.dig(:ai, :tui, :theme)
|
|
394
428
|
[nil, {}, 'invalid'].each do |configured|
|
|
@@ -1012,6 +1046,86 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
1012
1046
|
console.draw
|
|
1013
1047
|
expect(painted.map { |row| row[2] }.join).to include('SESSION · active')
|
|
1014
1048
|
expect(screen).to have_received(:setpos).with(a_kind_of(Integer), 2)
|
|
1049
|
+
console.handle("\u0014")
|
|
1050
|
+
painted.clear
|
|
1051
|
+
console.draw
|
|
1052
|
+
expect(painted.map { |row| row[2] }.join).to include(' PLAY ')
|
|
1053
|
+
expect(painted.map { |row| row[2] }.join).not_to include('MISSION CONTROL · active')
|
|
1054
|
+
expect(screen).to have_received(:setpos).with(1, 1)
|
|
1055
|
+
allow(curses).to receive_messages(lines: 24, cols: 80)
|
|
1056
|
+
console.draw
|
|
1057
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
it 'cycles mission, session and visible animation with both shortcuts without changing the draft' do
|
|
1061
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1062
|
+
editor = console.instance_variable_get(:@editor)
|
|
1063
|
+
editor.place('kept draft', 4)
|
|
1064
|
+
console.instance_variable_set(:@header_pane_height, 10)
|
|
1065
|
+
console.banner_frame(8, cells: true)
|
|
1066
|
+
console.handle("\u0014")
|
|
1067
|
+
console.handle("\u0018")
|
|
1068
|
+
expect(console.instance_variable_get(:@focus)).to eq(:animation)
|
|
1069
|
+
console.banner_frame(8, cells: true)
|
|
1070
|
+
game = console.instance_variable_get(:@banner_game)
|
|
1071
|
+
expect(game).to receive(:key).with(:up)
|
|
1072
|
+
expect(game).to receive(:key).with(' ')
|
|
1073
|
+
console.handle(:up)
|
|
1074
|
+
console.handle(' ')
|
|
1075
|
+
expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
|
|
1076
|
+
console.handle("\u0014")
|
|
1077
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1078
|
+
expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
it 'returns to the unchanged automatic animation when focus leaves the game' do
|
|
1082
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1083
|
+
allow(Process).to receive(:clock_gettime).and_return(10.0)
|
|
1084
|
+
automatic = console.banner_frame(8, cells: true)
|
|
1085
|
+
console.instance_variable_set(:@header_pane_height, 10)
|
|
1086
|
+
2.times { console.handle("\u0014") }
|
|
1087
|
+
expect(console.banner_frame(8, cells: true)).not_to eq(automatic)
|
|
1088
|
+
console.handle("\u0018")
|
|
1089
|
+
expect(console.banner_frame(8, cells: true)).to eq(automatic)
|
|
1090
|
+
console.instance_variable_set(:@header_pane_height, nil)
|
|
1091
|
+
2.times { console.handle("\u0014") }
|
|
1092
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
it 'keeps modal, search and cancellation priority above animation controls' do
|
|
1096
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1097
|
+
console.instance_variable_set(:@focus, :animation)
|
|
1098
|
+
editor = console.instance_variable_get(:@editor)
|
|
1099
|
+
editor.place('draft', 2)
|
|
1100
|
+
game = double(key: nil)
|
|
1101
|
+
console.instance_variable_set(:@banner_game, game)
|
|
1102
|
+
console.instance_variable_set(:@details, 0)
|
|
1103
|
+
expect(console).to receive(:handle_details).with(:up)
|
|
1104
|
+
console.handle(:up)
|
|
1105
|
+
console.instance_variable_set(:@details, nil)
|
|
1106
|
+
console.instance_variable_set(:@model_prompt, {})
|
|
1107
|
+
expect(console).to receive(:handle_model_prompt).with(' ')
|
|
1108
|
+
console.handle(' ')
|
|
1109
|
+
console.instance_variable_set(:@model_prompt, nil)
|
|
1110
|
+
allow(console).to receive(:busy?).and_return(true)
|
|
1111
|
+
expect(console).to receive(:cancel)
|
|
1112
|
+
console.handle("\u0003")
|
|
1113
|
+
console.handle("\u0012")
|
|
1114
|
+
expect(editor.search).not_to be_nil
|
|
1115
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1116
|
+
expect(game).not_to have_received(:key)
|
|
1117
|
+
expect([editor.text, editor.cursor]).to eq(['draft', 2])
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
it 'preserves bracketed paste and the mission cursor when leaving animation focus' do
|
|
1121
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1122
|
+
editor = console.instance_variable_get(:@editor)
|
|
1123
|
+
editor.place('draft', 2)
|
|
1124
|
+
console.instance_variable_set(:@focus, :animation)
|
|
1125
|
+
[:paste_start, ' ', "\n", 'x', :paste_end].each { |key| console.handle(key) }
|
|
1126
|
+
expect(editor.text).to eq("dr \nxaft")
|
|
1127
|
+
expect(editor.cursor).to eq(5)
|
|
1128
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1015
1129
|
end
|
|
1016
1130
|
|
|
1017
1131
|
it 'toggles the pane with Ctrl+T and opens swarm with Ctrl+S' do
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -403,6 +403,7 @@
|
|
|
403
403
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
|
|
404
404
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.duration_unsatisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.duration_unsatisfied?`: "}]}
|
|
405
405
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
|
|
406
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_thinking! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_thinking!`: "}]}
|
|
406
407
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.engine_transient? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.engine_transient?`: "}]}
|
|
407
408
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
|
|
408
409
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_blob Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_blob`: "}]}
|
|
@@ -486,9 +487,10 @@
|
|
|
486
487
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.wrap_untrusted_tool Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.wrap_untrusted_tool`: "}]}
|
|
487
488
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_or_read_evidenced? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_or_read_evidenced?`: "}]}
|
|
488
489
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
|
|
490
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.model`: "}]}
|
|
491
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel`: "}]}
|
|
489
492
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.new`: "}]}
|
|
490
493
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.checkpoint Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.checkpoint`: "}]}
|
|
491
|
-
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.model`: "}]}
|
|
492
494
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.submit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.submit`: "}]}
|
|
493
495
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.with_reader Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.with_reader`: "}]}
|
|
494
496
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering::Restart.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering::Restart.new`: "}]}
|
|
@@ -1206,6 +1208,8 @@
|
|
|
1206
1208
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.ollama_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.ollama_rest_call`: Supported Method Parameters\n\nollama_rest_call(\n\nhttp_method: 'optional HTTP method (defaults to GET)',\nrest_call: 'required rest call path relative to base_uri (e.g. api/chat)',\nparams: 'optional params passed in the URI or HTTP Headers',\nhttp_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
|
|
1207
1209
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.parse_tool_arguments_object Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.parse_tool_arguments_object`: "}]}
|
|
1208
1210
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.real_config_value?`: "}]}
|
|
1211
|
+
{"messages":[{"role":"user","content":"PWN::AI::Ollama.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.strict_system_template?`: "}]}
|
|
1212
|
+
{"messages":[{"role":"user","content":"PWN::AI::Ollama.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.template_rejects_later_system?`: "}]}
|
|
1209
1213
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.visible_from_thinking`: "}]}
|
|
1210
1214
|
{"messages":[{"role":"user","content":"PWN::AI::OpenAI.api_endpoint Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.api_endpoint`: "}]}
|
|
1211
1215
|
{"messages":[{"role":"user","content":"PWN::AI::OpenAI.authors Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
@@ -1258,6 +1262,8 @@
|
|
|
1258
1262
|
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.openwebui_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.openwebui_rest_call`: Supported Method Parameters\n\nopenwebui_rest_call(\n\nhttp_method: 'optional HTTP method (defaults to GET)',\nrest_call: 'required rest call path relative to base_uri',\nparams: 'optional params passed in the URI or HTTP Headers',\nhttp_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
|
|
1259
1263
|
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.parse_tool_arguments_object Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.parse_tool_arguments_object`: "}]}
|
|
1260
1264
|
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.real_config_value?`: "}]}
|
|
1265
|
+
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.strict_system_template?`: "}]}
|
|
1266
|
+
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.template_rejects_later_system?`: "}]}
|
|
1261
1267
|
{"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.visible_from_thinking`: "}]}
|
|
1262
1268
|
{"messages":[{"role":"user","content":"PWN::AI::RedTeam.authors Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1263
1269
|
{"messages":[{"role":"user","content":"PWN::AI::RedTeam.help Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.help`: "}]}
|
|
@@ -1729,6 +1735,7 @@
|
|
|
1729
1735
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids`: "}]}
|
|
1730
1736
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids_snapshot`: "}]}
|
|
1731
1737
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_block_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_board`: "}]}
|
|
1738
|
+
{"messages":[{"role":"user","content":"PWN::Banner.mini_block_plan Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_plan`: "}]}
|
|
1732
1739
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_cells Usage"},{"role":"assistant","content":"`PWN::Banner.mini_cells`: Supported Method Parameters\n\nReturn rows of single-column Unicode block cells, with named terminal colors. Each fresh cell has glyph, foreground and background keys; no ANSI or IO. Square means terminal-cell dimensions, not physical pixels. Caller owns borders. Use mini_frame_seconds for cadence; 1800 frames span 90 or 180 seconds. Colored blocks fill the canvas with half-cell pieces; other games cap at 16. PWN::Banner.mini_cells(\n\nname: 'optional - falling_blocks, snake, pong, or asteroids; defaults to falling_blocks',\nframe: 'optional - integer index, defaults to zero and wraps at 1800',\nwidth: 'optional - columns clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nheight: 'optional - rows clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nseed: 'optional - integer local PRNG seed, defaults to 73; retain per session',\nbranding: 'optional - include the legacy wordmark, defaults to true; false reclaims its row'\n\n)\n"}]}
|
|
1733
1740
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_delta Usage"},{"role":"assistant","content":"`PWN::Banner.mini_delta`: "}]}
|
|
1734
1741
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_emit Usage"},{"role":"assistant","content":"`PWN::Banner.mini_emit`: "}]}
|
|
@@ -1740,6 +1747,7 @@
|
|
|
1740
1747
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_landing Usage"},{"role":"assistant","content":"`PWN::Banner.mini_landing`: "}]}
|
|
1741
1748
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_names Usage"},{"role":"assistant","content":"`PWN::Banner.mini_names`: Supported Method Parameters\n\nList animation names; callers may sample once and retain the session choice. PWN::Banner.mini_names\n"}]}
|
|
1742
1749
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pixel Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pixel`: "}]}
|
|
1750
|
+
{"messages":[{"role":"user","content":"PWN::Banner.mini_play_piece Usage"},{"role":"assistant","content":"`PWN::Banner.mini_play_piece`: "}]}
|
|
1743
1751
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pong Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong`: "}]}
|
|
1744
1752
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pong_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong_snapshot`: "}]}
|
|
1745
1753
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_quarter_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_quarter_board`: "}]}
|
|
@@ -1781,6 +1789,13 @@
|
|
|
1781
1789
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.get Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.get`: Supported Method Parameters\n\nPWN::Banner::Matrix.get\n"}]}
|
|
1782
1790
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.help Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.help`: "}]}
|
|
1783
1791
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.you? Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.you?`: Supported Method Parameters\n\nPWN::Banner::Matrix.has_you\n"}]}
|
|
1792
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.new Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.new`: "}]}
|
|
1793
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.<< Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.<<`: "}]}
|
|
1794
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.cells Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.cells`: "}]}
|
|
1795
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.controls Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.controls`: "}]}
|
|
1796
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.key Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.key`: "}]}
|
|
1797
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.length Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.length`: "}]}
|
|
1798
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.step Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.step`: "}]}
|
|
1784
1799
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.authors Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1785
1800
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.get Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.get`: Supported Method Parameters\n\nPWN::Banner::Ninja.get\n"}]}
|
|
1786
1801
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.help Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.help`: "}]}
|
|
@@ -3334,6 +3349,7 @@
|
|
|
3334
3349
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_lines`: "}]}
|
|
3335
3350
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_text Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_text`: "}]}
|
|
3336
3351
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle`: "}]}
|
|
3352
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_animation Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_animation`: "}]}
|
|
3337
3353
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_details Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_details`: "}]}
|
|
3338
3354
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt`: "}]}
|
|
3339
3355
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_search Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_search`: "}]}
|
|
@@ -3349,6 +3365,7 @@
|
|
|
3349
3365
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_swarm`: "}]}
|
|
3350
3366
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_system_role Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_system_role`: "}]}
|
|
3351
3367
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.operation_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.operation_lines`: "}]}
|
|
3368
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.playable_banner Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.playable_banner`: "}]}
|
|
3352
3369
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission`: "}]}
|
|
3353
3370
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put`: "}]}
|
|
3354
3371
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put_spans Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put_spans`: "}]}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.760
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -505,14 +505,14 @@ dependencies:
|
|
|
505
505
|
requirements:
|
|
506
506
|
- - '='
|
|
507
507
|
- !ruby/object:Gem::Version
|
|
508
|
-
version: 1.
|
|
508
|
+
version: 1.7.0
|
|
509
509
|
type: :runtime
|
|
510
510
|
prerelease: false
|
|
511
511
|
version_requirements: !ruby/object:Gem::Requirement
|
|
512
512
|
requirements:
|
|
513
513
|
- - '='
|
|
514
514
|
- !ruby/object:Gem::Version
|
|
515
|
-
version: 1.
|
|
515
|
+
version: 1.7.0
|
|
516
516
|
- !ruby/object:Gem::Dependency
|
|
517
517
|
name: meshtastic
|
|
518
518
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -771,14 +771,14 @@ dependencies:
|
|
|
771
771
|
requirements:
|
|
772
772
|
- - '='
|
|
773
773
|
- !ruby/object:Gem::Version
|
|
774
|
-
version: 1.
|
|
774
|
+
version: 1.7.0
|
|
775
775
|
type: :development
|
|
776
776
|
prerelease: false
|
|
777
777
|
version_requirements: !ruby/object:Gem::Requirement
|
|
778
778
|
requirements:
|
|
779
779
|
- - '='
|
|
780
780
|
- !ruby/object:Gem::Version
|
|
781
|
-
version: 1.
|
|
781
|
+
version: 1.7.0
|
|
782
782
|
- !ruby/object:Gem::Dependency
|
|
783
783
|
name: pry
|
|
784
784
|
requirement: !ruby/object:Gem::Requirement
|