pwn 0.5.760 → 0.5.761
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/documentation/pwn-ai-Agent.md +44 -10
- data/etc/default_skills/pwn/banner/SKILL.md +1 -1
- data/lib/pwn/banner.rb +593 -53
- data/lib/pwn/plugins/repl/ai_console.rb +46 -9
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/banner_spec.rb +428 -20
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +167 -6
- data/third_party/pwn_rdoc.jsonl +23 -2
- metadata +1 -1
|
@@ -105,6 +105,8 @@ module PWN
|
|
|
105
105
|
|
|
106
106
|
# Read a duplicated terminal rather than stdin, which belongs to tools.
|
|
107
107
|
class Keyboard
|
|
108
|
+
attr_writer :interval
|
|
109
|
+
|
|
108
110
|
KEYS = {
|
|
109
111
|
"\e[A" => :up, "\e[B" => :down, "\e[C" => :right, "\e[D" => :left,
|
|
110
112
|
"\eOA" => :up, "\eOB" => :down, "\eOC" => :right, "\eOD" => :left,
|
|
@@ -120,7 +122,7 @@ module PWN
|
|
|
120
122
|
end
|
|
121
123
|
|
|
122
124
|
def call
|
|
123
|
-
if @source.wait_readable(@buffer.empty? ? 0.04 : 0)
|
|
125
|
+
if @source.wait_readable(@buffer.empty? ? (@interval || 0.04) : 0)
|
|
124
126
|
chunk = @source.read_nonblock(4096, exception: false)
|
|
125
127
|
return :eof if chunk.nil? && @buffer.empty?
|
|
126
128
|
|
|
@@ -402,6 +404,7 @@ module PWN
|
|
|
402
404
|
break if @leaving && !busy?
|
|
403
405
|
|
|
404
406
|
draw
|
|
407
|
+
@getch.interval = @focus == :animation && %i[asteroids galaga frogger].include?(@banner_name) ? 0.01 : 0.04 if @getch.is_a?(Keyboard)
|
|
405
408
|
handle(@getch.call)
|
|
406
409
|
rescue Interrupt
|
|
407
410
|
cancel
|
|
@@ -690,7 +693,7 @@ module PWN
|
|
|
690
693
|
|
|
691
694
|
return clear_view if key == "\u000c"
|
|
692
695
|
|
|
693
|
-
if
|
|
696
|
+
if key == "\u0013" || (key == "\u0007" && (@focus != :animation || @workspace))
|
|
694
697
|
@workspace ? @workspace = nil : open_swarm
|
|
695
698
|
return
|
|
696
699
|
end
|
|
@@ -801,12 +804,22 @@ module PWN
|
|
|
801
804
|
end
|
|
802
805
|
|
|
803
806
|
def handle_animation(key)
|
|
804
|
-
if key ==
|
|
807
|
+
if key == "\u0007"
|
|
808
|
+
names = PWN::Banner.mini_names
|
|
809
|
+
@banner_name = names[(names.index(@banner_name).to_i + 1) % names.length]
|
|
810
|
+
@banner_frame_seconds = PWN::Banner.mini_frame_seconds(name: @banner_name)
|
|
811
|
+
@banner_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
812
|
+
@banner_game = @banner_game_key = @banner_game_tick = nil
|
|
813
|
+
elsif key == :paste_start
|
|
805
814
|
@focus = :mission
|
|
806
815
|
@paste = true
|
|
807
816
|
@banner_game&.controls
|
|
808
817
|
elsif [:up, :down, :left, :right, ' '].include?(key)
|
|
818
|
+
return if %i[asteroids galaga frogger].include?(@banner_name) && @banner_game_frame&.dig(:state, :explosion).to_i.positive?
|
|
819
|
+
|
|
809
820
|
@banner_game&.key(key)
|
|
821
|
+
# Paint a discrete control immediately, not after the next idle tick.
|
|
822
|
+
@banner_game_tick = nil if %i[asteroids galaga frogger].include?(@banner_name)
|
|
810
823
|
elsif ["\r", "\n", "\e", :newline].include?(key)
|
|
811
824
|
@focus = :mission
|
|
812
825
|
@banner_game&.controls
|
|
@@ -1053,7 +1066,7 @@ module PWN
|
|
|
1053
1066
|
['REQUEST', state, "Elapsed #{elapsed}", "Completed tools #{@completed_tools}",
|
|
1054
1067
|
"Events #{@event_count}", '', 'TOKENS', "#{usage[:input_tokens]} in · #{usage[:output_tokens]} out",
|
|
1055
1068
|
"cached #{usage[:cached_tokens]} · calls #{usage[:calls]}", "Cost #{cost}",
|
|
1056
|
-
'', 'SWARM', swarm[:swarm_id] || 'none', "#{active} active · ^
|
|
1069
|
+
'', 'SWARM', swarm[:swarm_id] || 'none', "#{active} active · ^S swarm",
|
|
1057
1070
|
'', 'VIEW', @verbose ? 'verbose' : 'compact · /verbose', scroll_status,
|
|
1058
1071
|
'', 'LAST TOOL', @last_tool || 'not observed']
|
|
1059
1072
|
end
|
|
@@ -1226,6 +1239,7 @@ module PWN
|
|
|
1226
1239
|
cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
|
|
1227
1240
|
frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
|
|
1228
1241
|
return playable_banner(size, now) if cells && @focus == :animation
|
|
1242
|
+
return automatic_banner(size, now) if cells && size >= 5 && %i[pacman galaga frogger].include?(@banner_name)
|
|
1229
1243
|
return PWN::Banner.mini_cells(name: @banner_name, frame: frame, width: size, height: size, seed: @banner_seed, branding: false) if cells
|
|
1230
1244
|
|
|
1231
1245
|
art = PWN::Banner.mini_frame(name: @banner_name, frame: frame, width: size, height: size, branding: false)
|
|
@@ -1235,6 +1249,24 @@ module PWN
|
|
|
1235
1249
|
canvas
|
|
1236
1250
|
end
|
|
1237
1251
|
|
|
1252
|
+
def automatic_banner(size, now)
|
|
1253
|
+
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1254
|
+
if @banner_demo_key != key
|
|
1255
|
+
@banner_demo_key = key
|
|
1256
|
+
@banner_demo = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed, demo: true)
|
|
1257
|
+
@banner_demo.step
|
|
1258
|
+
@banner_demo_tick = nil
|
|
1259
|
+
end
|
|
1260
|
+
tick = (now / @banner_frame_seconds).floor
|
|
1261
|
+
if @banner_demo_tick != tick
|
|
1262
|
+
# Advance one frame, never build 1800 or catch up after a modal.
|
|
1263
|
+
@banner_demo_frame = @banner_demo.step
|
|
1264
|
+
@banner_demo_cells = @banner_demo.cells(@banner_demo_frame)
|
|
1265
|
+
@banner_demo_tick = tick
|
|
1266
|
+
end
|
|
1267
|
+
@banner_demo_cells
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1238
1270
|
def playable_banner(size, now)
|
|
1239
1271
|
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1240
1272
|
if @banner_game_key != key
|
|
@@ -1242,9 +1274,13 @@ module PWN
|
|
|
1242
1274
|
@banner_game = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed)
|
|
1243
1275
|
@banner_game_tick = nil
|
|
1244
1276
|
end
|
|
1245
|
-
|
|
1277
|
+
# Human Snake input needs a full reaction interval on its tiny board;
|
|
1278
|
+
# decorative replay cadence is independent of player movement.
|
|
1279
|
+
cadence = @banner_name == :snake ? 0.2 : @banner_frame_seconds
|
|
1280
|
+
tick = (now / cadence).floor
|
|
1246
1281
|
if @banner_game_tick != tick
|
|
1247
|
-
@
|
|
1282
|
+
@banner_game_frame = @banner_game.step
|
|
1283
|
+
@banner_game_cells = @banner_game.cells(@banner_game_frame)
|
|
1248
1284
|
@banner_game_tick = tick
|
|
1249
1285
|
end
|
|
1250
1286
|
@banner_game_cells
|
|
@@ -1308,9 +1344,10 @@ module PWN
|
|
|
1308
1344
|
def footer_text
|
|
1309
1345
|
text = '/ menu · ^C=cancel · ^D=back · ^L=clear · ^T=toggle pane · ^S=swarm · ^R=search ^O=operations · ↑↓ history · Shift+Enter=newline · Enter=send'
|
|
1310
1346
|
if @focus == :animation
|
|
1311
|
-
controls = {
|
|
1312
|
-
asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire'
|
|
1313
|
-
|
|
1347
|
+
controls = { pacman: '↑↓←→ start/steer · ● power', snake: '↑↓←→ start/steer', falling_blocks: '←→ move · ↓ faster · Space=rotate',
|
|
1348
|
+
asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire', galaga: '↑↓←→ move · Space=fire',
|
|
1349
|
+
frogger: '↑↓←→ hop · logs → goals' }.fetch(@banner_name, '↑↓←→ play')
|
|
1350
|
+
return text.sub('↑↓ history', "^G=next game · #{controls}").sub('Shift+Enter=newline · Enter=send', 'Enter/Esc=draft')
|
|
1314
1351
|
end
|
|
1315
1352
|
@focus == :session ? text.sub('↑↓ history', '↑↓ scroll · HOME · PGUP · PGDN · END') : text
|
|
1316
1353
|
end
|
data/lib/pwn/version.rb
CHANGED