pwn 0.5.749 → 0.5.751

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/documentation/Blockchain.md +107 -9
  4. data/documentation/pwn-ai-Agent.md +310 -4
  5. data/documentation/pwn_silent_help_learn_demo.gif +0 -0
  6. data/etc/default_skills/pwn/banner/SKILL.md +7 -3
  7. data/etc/default_skills/pwn/blockchain/btc/SKILL.md +8 -5
  8. data/etc/default_skills/pwn/blockchain/eth/SKILL.md +12 -3
  9. data/etc/default_skills/pwn/blockchain/eth/references/urls.md +1 -1
  10. data/etc/default_skills/pwn/plugins/repl/SKILL.md +5 -0
  11. data/etc/default_skills/pwn/plugins/repl/ai/SKILL.md +4 -0
  12. data/etc/default_skills/pwn/plugins/repl/ai/references/urls.md +5 -0
  13. data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +48 -0
  14. data/etc/default_skills/pwn/plugins/repl/ai_console_commands/SKILL.md +47 -0
  15. data/etc/default_skills/pwn/plugins/repl/ai_console_usage/SKILL.md +48 -0
  16. data/etc/default_skills/pwn/plugins/repl/ai_swarm/SKILL.md +46 -0
  17. data/lib/pwn/ai/agent/loop.rb +287 -18
  18. data/lib/pwn/ai/agent/swarm.rb +28 -4
  19. data/lib/pwn/ai/open_ai.rb +4 -2
  20. data/lib/pwn/banner.rb +743 -2
  21. data/lib/pwn/blockchain/btc.rb +494 -196
  22. data/lib/pwn/blockchain/eth.rb +440 -81
  23. data/lib/pwn/blockchain.rb +6 -0
  24. data/lib/pwn/config.rb +22 -0
  25. data/lib/pwn/migrate.rb +9 -1
  26. data/lib/pwn/plugins/repl/ai.rb +182 -17
  27. data/lib/pwn/plugins/repl/ai_console.rb +1581 -0
  28. data/lib/pwn/plugins/repl/ai_console_commands.rb +282 -0
  29. data/lib/pwn/plugins/repl/ai_console_usage.rb +144 -0
  30. data/lib/pwn/plugins/repl/ai_swarm.rb +375 -0
  31. data/lib/pwn/plugins/repl.rb +25 -3
  32. data/lib/pwn/version.rb +1 -1
  33. data/spec/lib/pwn/ai/agent/loop_spec.rb +258 -1
  34. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +3 -2
  35. data/spec/lib/pwn/ai/open_ai_spec.rb +5 -0
  36. data/spec/lib/pwn/banner_spec.rb +643 -0
  37. data/spec/lib/pwn/blockchain/btc_spec.rb +372 -1
  38. data/spec/lib/pwn/blockchain/eth_spec.rb +205 -0
  39. data/spec/lib/pwn/blockchain_spec.rb +6 -0
  40. data/spec/lib/pwn/config_spec.rb +10 -0
  41. data/spec/lib/pwn/migrate_spec.rb +40 -3
  42. data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +65 -0
  43. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +1434 -0
  44. data/spec/lib/pwn/plugins/repl/ai_console_usage_spec.rb +29 -0
  45. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +127 -0
  46. data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +5 -3
  47. data/spec/lib/pwn/plugins/repl_spec.rb +167 -1
  48. data/third_party/pwn_rdoc.jsonl +229 -10
  49. metadata +18 -6
  50. data/etc/default_skills/pwn/blockchain/btc/references/urls.md +0 -3
@@ -80,7 +80,7 @@ describe PWN::Migrate do
80
80
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 2))
81
81
  expect(described_class.needed?).to be(true)
82
82
  result = described_class.run(fix: false, backup: false, io: io)
83
- expect(result[:applied_migrations]).to eq([3, 4, 5])
83
+ expect(result[:applied_migrations]).to eq([3, 4, 5, 6, 7])
84
84
  expected = Marshal.load(Marshal.dump(original))
85
85
  expected['custom']['model'] = nil
86
86
  expect(YAML.safe_load_file(path)).to eq(expected)
@@ -189,7 +189,7 @@ describe PWN::Migrate do
189
189
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 3))
190
190
  expect(described_class.needed?).to be(true)
191
191
  result = described_class.run(fix: true, backup: false, io: io)
192
- expect(result[:applied_migrations]).to eq([4, 5])
192
+ expect(result[:applied_migrations]).to eq([4, 5, 6, 7])
193
193
  creds = YAML.safe_load_file(dec, symbolize_names: true)
194
194
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
195
195
  expect(user[:ai][:active]).to eq('grok')
@@ -215,7 +215,7 @@ describe PWN::Migrate do
215
215
  PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
216
216
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 4))
217
217
  result = described_class.run(fix: true, backup: false, io: io)
218
- expect(result[:applied_migrations]).to eq([5])
218
+ expect(result[:applied_migrations]).to eq([5, 6, 7])
219
219
  creds = YAML.safe_load_file(dec, symbolize_names: true)
220
220
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
221
221
  expect(user.dig(:ai, :agent, :skill_review)).to eq('off')
@@ -233,6 +233,43 @@ describe PWN::Migrate do
233
233
  expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([])
234
234
  end
235
235
 
236
+ it 'upgrades a schema-5 vault with ai.tui.theme without replacing a chosen color' do
237
+ yaml = File.join(@tmp, 'pwn.yaml')
238
+ dec = File.join(@tmp, 'pwn.yaml.decryptor')
239
+ stale = { ai: { active: 'grok', grok: { key: 'sk-KEEP' }, tui: { theme: { assistant: 'blue' } } } }
240
+ File.write(yaml, YAML.dump(stale).gsub(/^(\s*):/, '\1'))
241
+ PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
242
+ File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 5))
243
+ expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([6, 7])
244
+ creds = YAML.safe_load_file(dec, symbolize_names: true)
245
+ user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
246
+ theme = user.dig(:ai, :tui, :theme)
247
+ expect(theme[:assistant]).to eq('blue')
248
+ expect(theme[:border]).to eq('black')
249
+ expect(theme[:title]).to eq('red')
250
+ expect(theme[:request]).to eq('white')
251
+ expect(theme).to eq(PWN::Config.env_template.dig(:ai, :tui, :theme).merge(assistant: 'blue'))
252
+ expect(user.dig(:ai, :grok, :key)).to eq('sk-KEEP')
253
+ expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([])
254
+ expect(PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv]).dig(:ai, :tui, :theme)).to eq(theme)
255
+ end
256
+
257
+ it 'preserves every configured theme role when backfilling a current-schema vault' do
258
+ yaml = File.join(@tmp, 'pwn.yaml')
259
+ dec = File.join(@tmp, 'pwn.yaml.decryptor')
260
+ chosen = PWN::Config.env_template.dig(:ai, :tui, :theme).transform_values { 'blue' }
261
+ user = PWN::Config.env_template
262
+ user[:ai][:tui][:theme] = chosen
263
+ File.write(yaml, YAML.dump(user))
264
+ PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
265
+ File.write(File.join(@tmp, '.schema'), JSON.generate(schema: described_class::SCHEMA_VERSION))
266
+ encrypted = File.binread(yaml)
267
+ expect(described_class.backfill_vault(io: io)[:added]).to eq(0)
268
+ expect(File.binread(yaml)).to eq(encrypted)
269
+ creds = YAML.safe_load_file(dec, symbolize_names: true)
270
+ expect(PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv]).dig(:ai, :tui, :theme)).to eq(chosen)
271
+ end
272
+
236
273
  it 'dry_run writes nothing' do
237
274
  r = PWN::Migrate.run(fix: true, backup: true, dry_run: true, io: io)
238
275
  expect(r[:dry_run]).to be true
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PWN::Plugins::REPL::AIConsoleCommands do
6
+ it 'completes command names and parameter positions without a provider call' do
7
+ allow(PWN::Plugins::REPL).to receive(:pwn_ai_engines).and_return(%w[openai grok])
8
+ first = described_class.complete(line: '/mo', cursor: 3)
9
+ expect(first[:items].map { |item| item[:label] }).to include('/model')
10
+ expect(described_class.complete(line: '/', cursor: 1)[:items].map { |item| item[:label] }).to include('/system-role')
11
+ expect(described_class.complete(line: '/sys', cursor: 4)[:items].map { |item| item[:label] }).to eq(['/system-role'])
12
+ engines = described_class.complete(line: '/model ', cursor: 7)
13
+ expect(engines[:items].map { |item| item[:label] }).to include('openai', 'list')
14
+ expect(engines[:items].map { |item| item[:text] }).to include('/model openai')
15
+ verbose = described_class.complete(line: '/verbose ', cursor: 9)
16
+ expect(verbose[:items].map { |item| item[:label] }).to eq(%w[on off])
17
+ free = described_class.complete(line: '/steer keep ', cursor: 12)
18
+ expect(free[:items]).to be_empty
19
+ expect(free[:hint]).to include('free text')
20
+ end
21
+
22
+ it 'completes PWN constants and filesystem paths as they are typed' do
23
+ hits = described_class.complete(line: 'inspect PWN::Plug', cursor: 16)
24
+ expect(hits[:items].map { |item| item[:label] }).to include('PWN::Plugins::')
25
+ expect(hits[:hint]).to eq('PWN constant')
26
+ Dir.mktmpdir do |dir|
27
+ File.write(File.join(dir, 'sample.txt'), 'x')
28
+ path = described_class.complete(line: "#{dir}/sam", cursor: "#{dir}/sam".length)
29
+ expect(path[:items].map { |item| item[:label] }).to include(File.join(dir, 'sample.txt'))
30
+ expect(path[:hint]).to eq('path')
31
+ end
32
+ end
33
+
34
+ it 'does not autoload every constant candidate or fail on a scalar namespace' do
35
+ mod = Module.new
36
+ mod.autoload(:Unloaded, '/not/a/real/dependency.rb')
37
+ mod.const_set(:Scalar, 42)
38
+ stub_const('PWN::CompletionProbe', mod)
39
+ result = described_class.complete(line: 'PWN::CompletionProbe::', cursor: 22)
40
+ expect(result[:items].map { |item| item[:label] }).to include('PWN::CompletionProbe::Unloaded::')
41
+ expect(mod.autoload?(:Unloaded)).not_to be_nil
42
+ line = 'PWN::CompletionProbe::Scalar::'
43
+ expect(described_class.complete(line: line, cursor: line.length)[:items]).to be_empty
44
+ end
45
+
46
+ it 'recognizes absolute paths outside a hardcoded root list without swallowing slash commands' do
47
+ expect(PWN::Plugins::REPL).to receive(:pwn_ai_complete_path).with(target: '/srv/probe', line: '/srv/probe').and_return(['/srv/probe.txt'])
48
+ expect(described_class.complete(line: '/srv/probe', cursor: 10)[:items].first[:label]).to eq('/srv/probe.txt')
49
+ expect(described_class.complete(line: '/mo', cursor: 3)[:items].first[:label]).to eq('/model')
50
+ end
51
+
52
+ it 'preserves a Unicode suffix when replacing the token at the cursor' do
53
+ result = described_class.complete(line: '/verbose o café', cursor: 10)
54
+ chosen = result[:items].find { |item| item[:label] == 'on' }
55
+ expect(chosen[:text]).to eq('/verbose on café')
56
+ expect(chosen[:text].scan(/\X/)[chosen[:cursor]]).to eq(' ')
57
+ end
58
+
59
+ it 'keeps every command reachable and completes only the prefix before a mid-token cursor' do
60
+ labels = described_class.complete(line: '/', cursor: 1)[:items].map { |item| item[:label] }
61
+ expect(labels).to include('/swarm', '/verbose', '/steer', '/status')
62
+ result = described_class.complete(line: '/verbose ox café', cursor: 10)
63
+ expect(result[:items].map { |item| item[:text] }).to include('/verbose on café')
64
+ end
65
+ end