pwn 0.5.685 → 0.5.686

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.
@@ -544,114 +544,6 @@ module PWN
544
544
  raise e
545
545
  end
546
546
 
547
- # Supported Method Parameters::
548
- # usage = PWN::AI::OpenAI.get_plan_usage(
549
- # timeout: 'optional - seconds (default 8)'
550
- # )
551
- #
552
- # Subscription / billing usage for the PS1 percent suffix.
553
- # Tries (1) dashboard billing hard-limit vs month-to-date spend,
554
- # then (2) Admin organization costs. Returns {available:false}
555
- # when neither yields used+limit (never prompts for credentials).
556
- public_class_method def self.get_plan_usage(opts = {})
557
- timeout = opts[:timeout] || 8
558
- return { available: false, engine: :openai } unless plan_usage_credentials?
559
-
560
- now = Time.now.utc
561
- start_of_month = Time.utc(now.year, now.month, 1)
562
-
563
- sub = parse_plan_usage_json(
564
- raw: open_ai_rest_call(
565
- rest_call: 'dashboard/billing/subscription',
566
- timeout: timeout,
567
- spinner: false,
568
- quiet: true,
569
- non_interactive: true
570
- )
571
- )
572
- usage = parse_plan_usage_json(
573
- raw: open_ai_rest_call(
574
- rest_call: 'dashboard/billing/usage',
575
- params: {
576
- start_date: start_of_month.strftime('%Y-%m-%d'),
577
- end_date: now.strftime('%Y-%m-%d')
578
- },
579
- timeout: timeout,
580
- spinner: false,
581
- quiet: true,
582
- non_interactive: true
583
- )
584
- )
585
-
586
- if sub.is_a?(Hash) && usage.is_a?(Hash)
587
- limit_usd = (
588
- sub[:hard_limit_usd] ||
589
- sub[:system_hard_limit_usd] ||
590
- sub[:soft_limit_usd] ||
591
- sub[:system_soft_limit_usd]
592
- ).to_f
593
- used_usd = usage[:total_usage].to_f / 100.0
594
- normalized = PWN::AI.normalize_plan_usage(
595
- used: used_usd,
596
- limit: limit_usd,
597
- source: 'dashboard/billing',
598
- engine: :openai
599
- )
600
- return normalized if normalized[:available]
601
- end
602
-
603
- costs = parse_plan_usage_json(
604
- raw: open_ai_rest_call(
605
- rest_call: 'organization/costs',
606
- params: { start_time: start_of_month.to_i },
607
- timeout: timeout,
608
- spinner: false,
609
- quiet: true,
610
- non_interactive: true
611
- )
612
- )
613
- if costs.is_a?(Hash)
614
- spent = Array(costs[:data]).sum do |row|
615
- amt = row.is_a?(Hash) ? (row[:amount] || row[:results] || {}) : {}
616
- if amt.is_a?(Hash)
617
- (amt[:value] || amt[:amount] || 0).to_f
618
- else
619
- amt.to_f
620
- end
621
- end
622
- limit_usd = sub.is_a?(Hash) ? (sub[:hard_limit_usd] || sub[:system_hard_limit_usd]).to_f : 0.0
623
- normalized = PWN::AI.normalize_plan_usage(
624
- used: spent,
625
- limit: limit_usd,
626
- source: 'organization/costs',
627
- engine: :openai
628
- )
629
- return normalized if normalized[:available]
630
- end
631
-
632
- { available: false, engine: :openai }
633
- rescue StandardError
634
- { available: false, engine: :openai }
635
- end
636
-
637
- private_class_method def self.plan_usage_credentials?
638
- engine = PWN::Env.dig(:ai, :openai) || {}
639
- oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
640
- real_config_value?(value: engine[:key]) ||
641
- real_config_value?(value: oauth[:bearer_token]) ||
642
- real_config_value?(value: oauth[:refresh_token])
643
- end
644
-
645
- private_class_method def self.parse_plan_usage_json(opts = {})
646
- raw = opts[:raw]
647
- return nil if raw.nil?
648
-
649
- parsed = JSON.parse(raw.to_s, symbolize_names: true)
650
- parsed.is_a?(Hash) ? parsed : nil
651
- rescue JSON::ParserError, TypeError
652
- nil
653
- end
654
-
655
547
  # Supported Method Parameters::
656
548
  # response = PWN::AI::OpenAI.chat_with_tools(
657
549
  # messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
@@ -1287,8 +1179,6 @@ module PWN
1287
1179
  puts "USAGE:
1288
1180
  models = #{self}.get_models
1289
1181
 
1290
- usage = #{self}.get_plan_usage
1291
-
1292
1182
  # One-time ChatGPT/Codex OAuth enrollment (device-code flow):
1293
1183
  bearer = #{self}.obtain_oauth_bearer_token
1294
1184
  # Subsequent runs silently refresh via ai.openai.oauth.refresh_token
@@ -413,16 +413,6 @@ module PWN
413
413
  raise e
414
414
  end
415
415
 
416
- # Supported Method Parameters::
417
- # usage = PWN::AI::OpenWebUI.get_plan_usage
418
- #
419
- # Open WebUI is a local gateway with no subscription / plan-usage
420
- # endpoint. Always unavailable so the PS1 shows the infinity glyph.
421
- public_class_method def self.get_plan_usage(opts = {})
422
- _unused = opts
423
- { available: false, engine: :openwebui, unlimited: true }
424
- end
425
-
426
416
  # Coerce OpenAI-wire message history into Ollama-native shapes before
427
417
  # POST /api/chat (or Open WebUI /ollama/api/chat):
428
418
  # - function.arguments must be a Hash/Array object, not a JSON string
@@ -693,8 +683,6 @@ module PWN
693
683
  puts "USAGE:
694
684
  models = #{self}.get_models
695
685
 
696
- usage = #{self}.get_plan_usage
697
-
698
686
  response = #{self}.chat(
699
687
  request: 'required - message to Open WebUI',
700
688
  model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:openwebui][:model])',
data/lib/pwn/ai.rb CHANGED
@@ -14,92 +14,6 @@ module PWN
14
14
  autoload :OpenWebUI, 'pwn/ai/open_web_ui'
15
15
  autoload :RedTeam, 'pwn/ai/red_team'
16
16
 
17
- PLAN_USAGE_CACHE_TTL = 60
18
- PLAN_USAGE_INFINITY = "\u221E"
19
- ENGINE_MODULES = {
20
- openai: 'PWN::AI::OpenAI',
21
- grok: 'PWN::AI::Grok',
22
- ollama: 'PWN::AI::Ollama',
23
- openwebui: 'PWN::AI::OpenWebUI',
24
- anthropic: 'PWN::AI::Anthropic',
25
- gemini: 'PWN::AI::Gemini'
26
- }.freeze
27
-
28
- # Supported Method Parameters::
29
- # usage = PWN::AI.normalize_plan_usage(
30
- # used: 'required - numeric used amount',
31
- # limit: 'required - numeric limit / allowance',
32
- # source: 'optional - endpoint / origin label',
33
- # engine: 'optional - engine symbol'
34
- # )
35
- public_class_method def self.normalize_plan_usage(opts = {})
36
- used = opts[:used]
37
- limit = opts[:limit]
38
- used_f = used&.to_f
39
- limit_f = limit&.to_f
40
- available = !used_f.nil? && !limit_f.nil? && limit_f.positive?
41
- percent = available ? ((used_f / limit_f) * 100.0).round : nil
42
- percent = 0 if percent && percent.negative?
43
- percent = 100 if percent && percent > 100
44
- {
45
- available: available,
46
- used: used_f,
47
- limit: limit_f,
48
- percent: percent,
49
- source: opts[:source],
50
- engine: opts[:engine]
51
- }
52
- end
53
-
54
- # Supported Method Parameters::
55
- # usage = PWN::AI.plan_usage(
56
- # engine: 'optional - engine symbol (defaults to PWN::Env[:ai][:active])',
57
- # ttl: 'optional - cache seconds (default 60)',
58
- # force: 'optional - bypass cache (default false)'
59
- # )
60
- public_class_method def self.plan_usage(opts = {})
61
- engine = (opts[:engine] || PWN::Env.dig(:ai, :active)).to_s.downcase.to_sym
62
- ttl = (opts[:ttl] || PLAN_USAGE_CACHE_TTL).to_i
63
- ttl = PLAN_USAGE_CACHE_TTL if ttl <= 0
64
- cache = (@plan_usage_cache ||= {})
65
- now = Time.now.to_i
66
- hit = cache[engine]
67
- return hit[:value] if !opts[:force] && hit.is_a?(Hash) && (now - hit[:at].to_i) < ttl
68
-
69
- klass_name = ENGINE_MODULES[engine]
70
- value = { available: false, engine: engine }
71
- if klass_name
72
- begin
73
- klass = Object.const_get(klass_name)
74
- value = klass.get_plan_usage if klass.respond_to?(:get_plan_usage)
75
- value = { available: false, engine: engine } unless value.is_a?(Hash)
76
- value[:engine] ||= engine
77
- rescue StandardError
78
- value = { available: false, engine: engine }
79
- end
80
- end
81
- cache[engine] = { at: now, value: value }
82
- value
83
- end
84
-
85
- # Supported Method Parameters::
86
- # glyph = PWN::AI.plan_usage_glyph(
87
- # engine: 'optional - engine symbol (defaults to PWN::Env[:ai][:active])',
88
- # usage: 'optional - precomputed usage hash'
89
- # )
90
- #
91
- # Returns "19%" when used/limit are known, otherwise the infinity glyph.
92
- # Local engines (ollama / openwebui) always return infinity.
93
- public_class_method def self.plan_usage_glyph(opts = {})
94
- engine = (opts[:engine] || PWN::Env.dig(:ai, :active)).to_s.downcase.to_sym
95
- return PLAN_USAGE_INFINITY if %i[ollama openwebui].include?(engine)
96
-
97
- usage = opts[:usage] || plan_usage(engine: engine)
98
- return PLAN_USAGE_INFINITY unless usage.is_a?(Hash) && usage[:available] && !usage[:percent].nil?
99
-
100
- "#{usage[:percent].to_i}%"
101
- end
102
-
103
17
  # Display a List of Every PWN::AI Module
104
18
 
105
19
  # Author(s):: 0day Inc. <support@0dayinc.com>
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'json'
4
5
  require 'logger'
5
6
  require 'securerandom'
@@ -8,6 +9,35 @@ module PWN
8
9
  module Plugins
9
10
  # This plugin is used to instantiate a PWN logger with a custom message format
10
11
  module Log
12
+ TRACE_SKIP_METHODS = %i[
13
+ authors help to_s inspect class object_id hash eql? == equal? !
14
+ method public_send send __send__ instance_eval class_eval
15
+ public_class_method private_class_method
16
+ ].freeze
17
+ TRACE_SKIP_PREFIXES = %w[
18
+ PWN::Plugins::Log
19
+ PWN::Plugins::REPL
20
+ PWN::Banner
21
+ ].freeze
22
+ DEFAULT_TRACE_PREFIXES = %w[
23
+ PWN::AI
24
+ PWN::Memory
25
+ PWN::Sessions
26
+ PWN::Config
27
+ PWN::Cron
28
+ PWN::Plugins
29
+ ].freeze
30
+ SECRET_KEY_RX = /password|passwd|secret|token|api[_-]?key|authorization|bearer|cookie|session[_-]?id|private[_-]?key|decryptor|credential|ssh[_-]?key|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|vault|csrf/i
31
+ SECRET_VALUE_RX = %r{
32
+ -----BEGIN\ [A-Z ]*PRIVATE\ KEY----- |
33
+ Bearer\s+[A-Za-z0-9\-._~+/]+=* |
34
+ \beyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+ |
35
+ \b(?:sk|rk|pk|xai|xox[baprs]|ghp|gho|ghu|ghs|ghr|glpat|AKIA|ASIA|ya29)[-_][A-Za-z0-9\-_]{16,} |
36
+ \b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*\S+
37
+ }ix
38
+ DEBUG_VALUE_MAX = 240
39
+ DEBUG_ARGS_MAX = 1_800
40
+
11
41
  # Supported Method Parameters::
12
42
  # PWN::Log.create(
13
43
  # )
@@ -99,6 +129,271 @@ module PWN
99
129
  raise e
100
130
  end
101
131
 
132
+ public_class_method def self.debug_enabled?(opts = {})
133
+ return @debug_enabled == true if opts.is_a?(Hash)
134
+
135
+ @debug_enabled == true
136
+ end
137
+
138
+ public_class_method def self.debug_log_path(opts = {})
139
+ return @debug_path if opts.is_a?(Hash)
140
+
141
+ @debug_path
142
+ end
143
+
144
+ # Open /tmp/pwn-ai-DEBUG-TIMESTAMP.log, tee the same lines to the TUI
145
+ # (opts[:tee]), and TracePoint PWN modules that process a pwn-ai request.
146
+ DEBUG_LOG_MAX = 1_024_000
147
+
148
+ public_class_method def self.start_debug(opts = {})
149
+ return @debug_path if debug_enabled? && opts.is_a?(Hash)
150
+
151
+ ts = Time.now.strftime('%Y%m%d-%H%M%S')
152
+ path = opts[:path].to_s
153
+ path = "/tmp/pwn-ai-DEBUG-#{ts}.1.log" if path.empty?
154
+ stem, idx = debug_path_parts(path: path)
155
+ path = "#{stem}.#{idx}.log"
156
+ FileUtils.mkdir_p(File.dirname(path))
157
+ io = File.open(path, 'a')
158
+ io.sync = true
159
+ @debug_file = io
160
+ @debug_path = path
161
+ @debug_stem = stem
162
+ @debug_index = idx
163
+ @debug_tee = opts[:tee]
164
+ @debug_tui_quiet = false
165
+ @debug_enabled = true
166
+ start_trace!(prefixes: opts[:prefixes]) unless opts[:trace] == false
167
+ progress(msg: "debug session start path=#{path}", which_self: self)
168
+ path
169
+ end
170
+
171
+ public_class_method def self.stop_debug(opts = {})
172
+ reason = opts[:reason].to_s if opts.is_a?(Hash)
173
+ if debug_enabled?
174
+ tail = reason.to_s.empty? ? 'debug session stop' : "debug session stop reason=#{reason}"
175
+ progress(msg: tail, which_self: self)
176
+ end
177
+ stop_trace!
178
+ begin
179
+ @debug_file&.close unless @debug_file.nil? || @debug_file.closed?
180
+ rescue StandardError
181
+ nil
182
+ end
183
+ path = @debug_path
184
+ @debug_file = nil
185
+ @debug_path = nil
186
+ @debug_stem = nil
187
+ @debug_index = nil
188
+ @debug_tee = nil
189
+ @debug_tui_quiet = false
190
+ @debug_enabled = false
191
+ path
192
+ end
193
+
194
+ private_class_method def self.debug_path_parts(opts = {})
195
+ path = opts[:path].to_s
196
+ return [Regexp.last_match(1), Regexp.last_match(2).to_i] if path =~ /\A(.+)\.(\d+)\.log\z/
197
+
198
+ [path.sub(/\.log\z/, ''), 1]
199
+ end
200
+
201
+ private_class_method def self.roll_debug_log!(opts = {})
202
+ return if opts[:skip]
203
+ return if @debug_file.nil? || @debug_file.closed?
204
+ return if @debug_file.size < DEBUG_LOG_MAX
205
+
206
+ @debug_file.close
207
+ @debug_index = @debug_index.to_i + 1
208
+ path = "#{@debug_stem}.#{@debug_index}.log"
209
+ io = File.open(path, 'a')
210
+ io.sync = true
211
+ @debug_file = io
212
+ @debug_path = path
213
+ path
214
+ end
215
+
216
+ public_class_method def self.quiet_tui!(opts = {})
217
+ return if opts[:skip]
218
+
219
+ @debug_tui_quiet = true
220
+ end
221
+
222
+ public_class_method def self.loud_tui!(opts = {})
223
+ return if opts[:skip]
224
+
225
+ @debug_tui_quiet = false
226
+ end
227
+
228
+ # One progress line to the debug file and the TUI tee (same payload).
229
+ public_class_method def self.progress(opts = {})
230
+ return false unless debug_enabled?
231
+ return false if Thread.current[:pwn_log_progress]
232
+
233
+ Thread.current[:pwn_log_progress] = true
234
+ msg = sanitize_debug_text(text: opts[:msg].to_s)
235
+ which = opts[:which_self] || self
236
+ line = format_progress(msg: msg, which_self: which)
237
+ begin
238
+ @debug_file&.puts(line)
239
+ @debug_file&.flush
240
+ roll_debug_log!
241
+ rescue StandardError
242
+ nil
243
+ end
244
+ tee = opts.key?(:tee) ? opts[:tee] : @debug_tee
245
+ if !@debug_tui_quiet && tee.respond_to?(:puts)
246
+ begin
247
+ tee.puts(color_progress(line: line))
248
+ rescue StandardError
249
+ nil
250
+ end
251
+ end
252
+ true
253
+ ensure
254
+ Thread.current[:pwn_log_progress] = false
255
+ end
256
+
257
+ private_class_method def self.format_progress(opts = {})
258
+ msg = opts[:msg].to_s.tr("\n", ' ')[0, 4_000]
259
+ which = opts[:which_self]
260
+ who = if which.is_a?(Module)
261
+ (which.name || which.to_s).to_s
262
+ else
263
+ which.to_s
264
+ end
265
+ ts = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L%z')
266
+ bits = ["[DEBUG #{ts}]"]
267
+ bits << who unless who.empty?
268
+ bits << msg unless msg.empty?
269
+ sanitize_debug_text(text: bits.join(' '))
270
+ end
271
+
272
+ private_class_method def self.color_progress(opts = {})
273
+ line = opts[:line].to_s
274
+ "\001\e[35m\002#{line}\001\e[0m\002"
275
+ end
276
+
277
+ private_class_method def self.trace_module_name(opts = {})
278
+ tp = opts[:tp]
279
+ return '' unless tp
280
+
281
+ mod = tp.self.is_a?(Module) ? tp.self : tp.defined_class
282
+ name = if mod.respond_to?(:name) && mod.name
283
+ mod.name
284
+ else
285
+ mod.to_s
286
+ end
287
+ name.to_s.sub(/\A#<Class:(.+?)>\z/, '\1')
288
+ end
289
+
290
+ private_class_method def self.secret_key?(opts = {})
291
+ opts[:key].to_s.match?(SECRET_KEY_RX)
292
+ end
293
+
294
+ private_class_method def self.sanitize_debug_text(opts = {})
295
+ text = opts[:text].to_s
296
+ text = text.gsub(SECRET_VALUE_RX, '[REDACTED]')
297
+ text.gsub(/-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----/m, '[REDACTED]')
298
+ end
299
+
300
+ private_class_method def self.inspect_debug(opts = {})
301
+ val = opts[:value]
302
+ key = opts[:key]
303
+ return '[REDACTED]' if secret_key?(key: key)
304
+ return format_debug_hash(hash: val) if val.is_a?(Hash)
305
+ return '[REDACTED]' if val.is_a?(String) && val.match?(SECRET_VALUE_RX)
306
+
307
+ raw = sanitize_debug_text(text: val.inspect)
308
+ raw = "#{raw[0, DEBUG_VALUE_MAX]}…" if raw.length > DEBUG_VALUE_MAX
309
+ raw
310
+ rescue StandardError
311
+ "#<#{val.class}>"
312
+ end
313
+
314
+ private_class_method def self.format_debug_hash(opts = {})
315
+ hash = opts[:hash]
316
+ return '{}' unless hash.is_a?(Hash)
317
+
318
+ parts = hash.map do |k, v|
319
+ "#{k}: #{inspect_debug(value: v, key: k)}"
320
+ end
321
+ "{#{parts.join(', ')}}"
322
+ end
323
+
324
+ private_class_method def self.trace_arguments(opts = {})
325
+ tp = opts[:tp]
326
+ return '' unless tp
327
+
328
+ bind = tp.binding
329
+ parts = []
330
+ Array(tp.parameters).each do |kind, name|
331
+ next if name.nil?
332
+ next if %i[rest keyrest block].include?(kind)
333
+ next unless bind.local_variable_defined?(name)
334
+
335
+ val = bind.local_variable_get(name)
336
+ if name == :opts && val.is_a?(Hash)
337
+ rendered = format_debug_hash(hash: val)
338
+ return rendered == '{}' ? '' : rendered
339
+ end
340
+
341
+ parts << "#{name}=#{inspect_debug(value: val, key: name)}"
342
+ end
343
+ out = parts.join(', ')
344
+ out = "#{out[0, DEBUG_ARGS_MAX]}…" if out.length > DEBUG_ARGS_MAX
345
+ out
346
+ rescue StandardError
347
+ ''
348
+ end
349
+
350
+ private_class_method def self.format_trace_call(opts = {})
351
+ tp = opts[:tp]
352
+ klass = trace_module_name(tp: tp)
353
+ meth = tp.method_id
354
+ args = trace_arguments(tp: tp)
355
+ args.to_s.empty? ? "#{klass}.#{meth}" : "#{klass}.#{meth}(#{args})"
356
+ end
357
+
358
+ private_class_method def self.traceable?(opts = {})
359
+ tp = opts[:tp]
360
+ prefixes = opts[:prefixes] || DEFAULT_TRACE_PREFIXES
361
+ name = trace_module_name(tp: tp)
362
+ return false if name.empty?
363
+ return false if TRACE_SKIP_PREFIXES.any? { |p| name.start_with?(p) }
364
+ return false unless prefixes.any? { |p| name.start_with?(p) }
365
+ return false if TRACE_SKIP_METHODS.include?(tp.method_id)
366
+
367
+ true
368
+ end
369
+
370
+ public_class_method def self.start_trace!(opts = {})
371
+ return @debug_tp if @debug_tp && opts.is_a?(Hash)
372
+
373
+ prefixes = Array(opts[:prefixes] || DEFAULT_TRACE_PREFIXES).map(&:to_s)
374
+ prefixes = DEFAULT_TRACE_PREFIXES if prefixes.empty?
375
+ @debug_tp = TracePoint.new(:call) do |tp|
376
+ next unless debug_enabled?
377
+ next unless traceable?(tp: tp, prefixes: prefixes)
378
+
379
+ progress(
380
+ msg: format_trace_call(tp: tp),
381
+ which_self: ''
382
+ )
383
+ rescue StandardError
384
+ nil
385
+ end
386
+ @debug_tp.enable
387
+ @debug_tp
388
+ end
389
+
390
+ public_class_method def self.stop_trace!(opts = {})
391
+ return if opts[:skip]
392
+
393
+ @debug_tp&.disable
394
+ @debug_tp = nil
395
+ end
396
+
102
397
  # Author(s):: 0day Inc. <support@0dayinc.com>
103
398
 
104
399
  public_class_method def self.authors
@@ -116,6 +411,13 @@ module PWN
116
411
  msg: 'required - message to log',
117
412
  which_self: 'required - pass in self object from module calling #{self}'
118
413
  )
414
+
415
+ path = #{self}.start_debug(
416
+ tee: $stdout,
417
+ path: 'optional - defaults to /tmp/pwn-ai-DEBUG-TIMESTAMP.log'
418
+ )
419
+ #{self}.progress(msg: 'stage', which_self: self)
420
+ #{self}.stop_debug
119
421
  "
120
422
  end
121
423
  end
@@ -375,11 +375,10 @@ module PWN
375
375
  max_context = PWN::Env[:ai][engine][:max_prompt_length].to_i
376
376
  current_context_length = "#{PWN::Plugins::REPL.compact_context_tokens(tokens: used_tokens)}:" \
377
377
  "#{PWN::Plugins::REPL.compact_context_tokens(tokens: max_context)}"
378
- plan_usage_glyph = PWN::AI.plan_usage_glyph(engine: engine)
379
378
 
380
379
  pname = "pwn.ai:#{engine}"
381
- pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}/#{plan_usage_glyph}" if model
382
- pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}/#{plan_usage_glyph}.SPEAK" if pi.config.pwn_ai_speak
380
+ pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}" if model
381
+ pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}.SPEAK" if pi.config.pwn_ai_speak
383
382
  pi.config.prompt_name = pname
384
383
 
385
384
  name = "\001\e[1m\002\001\e[33m\002#{pi.config.prompt_name}\001\e[0m\002"
@@ -1045,11 +1044,23 @@ module PWN
1045
1044
  end
1046
1045
 
1047
1046
  Pry::Commands.create_command 'toggle-pwn-ai-debug' do
1048
- description 'Display the response_history object while using pwn.ai'
1047
+ description 'Stream pwn-ai module progress to the TUI and /tmp/pwn-ai-DEBUG-TIMESTAMP.log'
1049
1048
 
1050
1049
  def process
1051
1050
  pi = pry_instance
1052
- pi.config.pwn_ai_debug ? pi.config.pwn_ai_debug = false : pi.config.pwn_ai_debug = true
1051
+ if pi.config.pwn_ai_debug
1052
+ path = PWN::Plugins::Log.stop_debug
1053
+ pi.config.pwn_ai_debug = false
1054
+ if path
1055
+ output.puts "pwn-ai debug OFF (was #{path})"
1056
+ else
1057
+ output.puts 'pwn-ai debug OFF'
1058
+ end
1059
+ else
1060
+ path = PWN::Plugins::Log.start_debug(tee: output)
1061
+ pi.config.pwn_ai_debug = true
1062
+ output.puts "pwn-ai debug ON → #{path}"
1063
+ end
1053
1064
  end
1054
1065
  end
1055
1066
 
@@ -1071,7 +1082,7 @@ module PWN
1071
1082
  pi.config.pwn_asm = false if pi.config.pwn_asm
1072
1083
  pi.config.pwn_ai = false if pi.config.pwn_ai
1073
1084
  pi.config.pwn_ai_agent = false if pi.config.pwn_ai_agent
1074
- pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
1085
+ # pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
1075
1086
  pi.config.pwn_ai_speak = false if pi.config.pwn_ai_speak
1076
1087
  pi.config.completer = Pry::InputCompleter
1077
1088
  # pi.config.pwn_ai_original_input ||= Pry.config.input.clone
@@ -1215,10 +1226,19 @@ module PWN
1215
1226
  request: orig_request,
1216
1227
  session_id: sess_id,
1217
1228
  enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
1218
- on_tool: on_tool
1229
+ on_tool: on_tool,
1230
+ debug: pi.config.pwn_ai_debug,
1231
+ debug_tee: $stdout
1219
1232
  )
1233
+ $stdout.flush
1220
1234
  puts "\n\001\e[32m\002#{final}\001\e[0m\002\n\n"
1221
- pp PWN::Sessions.load(session_id: sess_id) if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
1235
+ $stdout.flush
1236
+ if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
1237
+ PWN::Plugins::Log.progress(
1238
+ msg: "session=#{sess_id}",
1239
+ which_self: PWN::Sessions
1240
+ )
1241
+ end
1222
1242
  request.replace('nil')
1223
1243
  next
1224
1244
  rescue StandardError => e
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.685'
4
+ VERSION = '0.5.686'
5
5
  end