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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4969508ce00cf858c5dfd06506e306f37d86e3d0671f07c3d3c021fa603b49d8
4
- data.tar.gz: 6e23201a56add7514b8a12de42ee930aba424e33329bc9c608bd3dc4c2ce0473
3
+ metadata.gz: 1c85ad9f56d45556168111931b475458009ad82fd7dac279457824484dde1105
4
+ data.tar.gz: 8cc423564b1f6adc95674b7002ca2a33abcc3e337b0d9a4dbf493a108431d1af
5
5
  SHA512:
6
- metadata.gz: 980664beb783d0a4c4aed5df5548d8f3c839e47afadd566a35f5b3b4413d6242e6c6da54319cacc91ceb7a3f8f5756f8268d29876b6277fdfda2e2f4475a251d
7
- data.tar.gz: de6ce8fac7cc8c1dda8989faef831c4c32bf88141d188b113aa16b7b0035f3062505fc4cb843ea2b7f9e179306f66060ab8fbc1e4c8a325169a274f5784ef79f
6
+ metadata.gz: f1b42ef466382cbe8122910f5baee95d4c1af40090bf31e26961f31ee2d66a44be805c51bed475c4f96b57c9a0fac00cacd7a2a554f1035fabd354823f4a4652
7
+ data.tar.gz: a53ae1eeb0fdd70938672b357c55456699810679921abd9ecfa17a1f296a3fe9c4b10dfd663b0958e52b450d80a7197c601724120efc9ccf03b90c0cac1d55b2
@@ -45,7 +45,7 @@ history # what you've typed → copy into a driver
45
45
  | `pwn-ai-sessions` | List / view / delete transcripts |
46
46
  | `pwn-ai-cron` | List / run scheduled jobs |
47
47
  | `pwn-ai-delegate` | Hand a task to a Swarm persona |
48
- | `toggle-pwn-ai-debug` | Show/hide raw tool_calls JSON |
48
+ | `toggle-pwn-ai-debug` | Stream pwn-ai module progress to the TUI and `/tmp/pwn-ai-DEBUG-TIMESTAMP.log` |
49
49
  | `toggle-pwn-ai-speaks` | TTS every final answer on/off |
50
50
  | `welcome-banner` | Redraw a random `PWN::Banner` |
51
51
  | `toggle-pager` | Pry pager on/off |
@@ -27,7 +27,7 @@ commands.
27
27
  | `pwn-ai-cron` | `PWN::Cron` | List/run/toggle scheduled jobs |
28
28
  | `pwn-ai-delegate` | `Agent::Swarm` | Send one request to a persona |
29
29
  | `pwn-irc` | *(deprecated)* | Prints a pointer to `Agent::Swarm` - the IRC daemon transport is gone |
30
- | `toggle-pwn-ai-debug` | `Agent::Loop` | Show/hide raw tool_calls JSON in the TUI |
30
+ | `toggle-pwn-ai-debug` | `Agent::Loop` / `Plugins::Log` | Stream every pwn-ai module step to the TUI and `/tmp/pwn-ai-DEBUG-TIMESTAMP.log` |
31
31
  | `toggle-pwn-ai-speaks` | `Plugins::Voice` | TTS every final answer on/off |
32
32
  | `welcome-banner` | `PWN::Banner` | Redraw a random banner |
33
33
  | `toggle-pager` | Pry | Page long output on/off |
@@ -62,6 +62,49 @@ module PWN
62
62
  unsatisfied incomplete_final empty_final evidence_final
63
63
  ].freeze
64
64
 
65
+ public_class_method def self.debug_on?(opts = {})
66
+ return true if opts[:debug]
67
+ return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
68
+
69
+ pry_on = defined?(Pry) && Pry.respond_to?(:config) &&
70
+ Pry.config.respond_to?(:pwn_ai_debug) && Pry.config.pwn_ai_debug
71
+ return true if pry_on
72
+
73
+ false
74
+ end
75
+
76
+ private_class_method def self.debug_progress(opts = {})
77
+ return false unless debug_on?(opts)
78
+ return false unless defined?(PWN::Plugins::Log)
79
+
80
+ PWN::Plugins::Log.progress(
81
+ msg: opts[:msg],
82
+ which_self: opts[:which_self] || self
83
+ )
84
+ end
85
+
86
+ private_class_method def self.start_debug_session(opts = {})
87
+ return unless debug_on?(opts)
88
+ return unless defined?(PWN::Plugins::Log)
89
+ return if PWN::Plugins::Log.debug_enabled?
90
+
91
+ PWN::Plugins::Log.start_debug(tee: opts[:debug_tee] || $stdout)
92
+ end
93
+
94
+ private_class_method def self.quiet_debug_tui!(opts = {})
95
+ return unless debug_on?(opts)
96
+ return unless defined?(PWN::Plugins::Log)
97
+
98
+ PWN::Plugins::Log.quiet_tui!(reason: opts[:reason])
99
+ end
100
+
101
+ private_class_method def self.loud_debug_tui!(opts = {})
102
+ return unless debug_on?(opts)
103
+ return unless defined?(PWN::Plugins::Log)
104
+
105
+ PWN::Plugins::Log.loud_tui!(reason: opts[:reason])
106
+ end
107
+
65
108
  private_class_method def self.dispatch_fail_n(opts = {})
66
109
  fails = opts[:turn_fails] || {}
67
110
  fails.sum do |key, count|
@@ -343,6 +386,7 @@ module PWN
343
386
  return false if request.match?(LOOKUP_REQUEST_RX)
344
387
  return false if request.match?(HOST_PATH_RX)
345
388
  return false if request.match?(BROWSER_REQUEST_RX)
389
+ return false if request.match?(HOWTO_RX)
346
390
  return false if request.match?(%r{\b(this\s+(?:host|machine|box|system|subnet|file|repo)|/opt/|implement|scan|hosts?)\b}i)
347
391
 
348
392
  request.match?(/\A(?:what|why|who|when|where|which|how)\b/i)
@@ -1095,6 +1139,9 @@ module PWN
1095
1139
  private_class_method def self.call_engine(opts = {})
1096
1140
  messages = opts[:messages]
1097
1141
  tools = opts[:tools]
1142
+ debug_progress(
1143
+ msg: "call_engine tools=#{tools.nil? ? 'none' : Array(tools).length} msgs=#{Array(messages).length}"
1144
+ )
1098
1145
 
1099
1146
  engine = active_engine
1100
1147
  mod_name = ENGINE_MODS[engine]
@@ -1569,6 +1616,8 @@ module PWN
1569
1616
  txt = txt.to_s.strip
1570
1617
  txt = 'I do not have enough context to answer that yet.' if txt.empty?
1571
1618
 
1619
+ debug_progress(msg: "final accepted chars=#{txt.length}")
1620
+ quiet_debug_tui!(reason: 'question')
1572
1621
  append_session(session_id: session_id, role: 'user', content: request)
1573
1622
  append_session(session_id: session_id, role: 'assistant', content: txt)
1574
1623
  if defined?(Learning) && should_auto_introspect?(local: local_engine?, turn_fails: {}, iter: 0)
@@ -1583,6 +1632,7 @@ module PWN
1583
1632
  end
1584
1633
  txt
1585
1634
  rescue StandardError => e
1635
+ quiet_debug_tui!(reason: 'question_error')
1586
1636
  warn "[pwn-ai/loop] answer_question swallowed: #{e.class}: #{e.message}"
1587
1637
  "Could not answer the question (#{e.class}: #{e.message})."
1588
1638
  end
@@ -1983,6 +2033,9 @@ module PWN
1983
2033
  request = opts[:request].to_s
1984
2034
  session_id = opts[:session_id]
1985
2035
  on_tool = opts[:on_tool]
2036
+ start_debug_session(opts)
2037
+ loud_debug_tui!(debug: opts[:debug])
2038
+ debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
1986
2039
  TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
1987
2040
  engine = active_engine
1988
2041
  local = local_engine?(engine: engine)
@@ -2003,21 +2056,22 @@ module PWN
2003
2056
  end
2004
2057
  Thread.current[:pwn_request_intent] = intent
2005
2058
  Thread.current[:pwn_extinguished] = {}
2059
+ debug_progress(msg: "intent=#{intent} engine=#{engine}", debug: opts[:debug])
2006
2060
  expose_current_session(session_id: session_id)
2007
2061
  Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
2008
2062
 
2009
2063
  cheap = opts[:force_tools] != true && %i[greeting howto recall].include?(intent)
2010
2064
 
2011
- # Greeting / light smalltalk: deterministic ack - no weather echo, no tools,
2012
- # no PromptBuilder, no Registry.
2013
2065
  if intent == :greeting && opts[:force_tools] != true
2066
+ debug_progress(msg: 'path=greeting', debug: opts[:debug])
2067
+ quiet_debug_tui!(debug: opts[:debug], reason: 'greeting')
2014
2068
  return answer_greeting(
2015
2069
  request: request,
2016
2070
  session_id: session_id
2017
2071
  )
2018
2072
  end
2019
2073
 
2020
- # Thin system prompt only for remaining cheap paths (howto/recall/statement/question).
2074
+ # Thin system prompt only for remaining cheap paths (howto/recall).
2021
2075
  if cheap
2022
2076
  system_role_content = opts[:system_role_content]
2023
2077
  if system_role_content.nil? || system_role_content.to_s.empty?
@@ -2028,16 +2082,18 @@ module PWN
2028
2082
  )
2029
2083
  opts[:system_role_content] = system_role_content
2030
2084
  end
2031
- # How-to: never enter plan_first / task recon / tool thrash.
2032
2085
  if intent == :howto
2086
+ debug_progress(msg: 'path=howto', debug: opts[:debug])
2087
+ quiet_debug_tui!(debug: opts[:debug], reason: 'howto')
2033
2088
  return answer_howto(
2034
2089
  request: request,
2035
2090
  session_id: session_id,
2036
2091
  system_role_content: system_role_content
2037
2092
  )
2038
2093
  end
2039
- # Pure prior-turn / vague memory recall: one cheap path, no plan_first.
2040
2094
  if intent == :recall
2095
+ debug_progress(msg: 'path=recall', debug: opts[:debug])
2096
+ quiet_debug_tui!(debug: opts[:debug], reason: 'recall')
2041
2097
  return answer_recall(
2042
2098
  request: request,
2043
2099
  session_id: session_id,
@@ -2088,8 +2144,10 @@ module PWN
2088
2144
  messages << { role: 'user', content: request }
2089
2145
  append_session(session_id: session_id, role: 'user', content: request)
2090
2146
 
2091
- # Every request gets a task compass.
2092
- task_summary_plan!(state: ts_state, request: request, on_tool: on_tool) if defined?(TaskSummarizer)
2147
+ trivia = world_knowledge?(request: request)
2148
+ # Every host-work request gets a task compass. Trivia does not —
2149
+ # inventing English tasks there keeps the model from emitting a final.
2150
+ task_summary_plan!(state: ts_state, request: request, on_tool: on_tool) if defined?(TaskSummarizer) && !trivia
2093
2151
  # Re-bind tools from English plan so task list is the sole driver of
2094
2152
  # tool exposure/ranking (Registry keyword router + CORE).
2095
2153
  if ts_state.is_a?(Hash) && defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:relevance_query)
@@ -2103,14 +2161,13 @@ module PWN
2103
2161
  )
2104
2162
  end
2105
2163
  end
2106
- # English-task-as-primary: inject tangible tasks only for autonomous goals.
2107
- inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
2164
+ # English-task-as-primary: inject tangible tasks only for host work.
2165
+ inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
2108
2166
  predicted = nil
2109
2167
  Thread.current[:pwn_plan_predicted] = nil
2110
2168
  cal_state = calibration_state
2111
2169
  force_plan = cal_state[:force_plan]
2112
- # Skip plan_first only for remaining cheap intents.
2113
- skip_plan = %i[howto recall greeting].include?(intent)
2170
+ skip_plan = %i[howto recall greeting].include?(intent) || trivia
2114
2171
  if !skip_plan && (force_plan || agent_flag(key: :plan_first, default: local) || budget_exhaustion_hot?) && !Array(tools).empty?
2115
2172
  predicted = plan_first(messages: messages, request: request, ts_state: ts_state)
2116
2173
  # P22 — prefer explicit return; fall back to thread stash
@@ -2129,7 +2186,7 @@ module PWN
2129
2186
  )
2130
2187
  end
2131
2188
  end
2132
- inject_task_focus!(messages: messages, state: ts_state, force: true, request: request)
2189
+ inject_task_focus!(messages: messages, state: ts_state, force: true, request: request) unless trivia
2133
2190
  end
2134
2191
  if budget_exhaustion_hot?
2135
2192
  english_open = defined?(TaskSummarizer) && TaskSummarizer.respond_to?(:plan_open?) &&
@@ -2166,7 +2223,7 @@ module PWN
2166
2223
  compact_history!(messages: messages) if local
2167
2224
  # English-task-as-primary: when plan_idx advanced, tell the model
2168
2225
  # which plain-English task is active before the next tool batch.
2169
- inject_task_focus!(messages: messages, state: ts_state, request: request)
2226
+ inject_task_focus!(messages: messages, state: ts_state, request: request) unless trivia
2170
2227
 
2171
2228
  # P17 — on the final iteration, strip tools and demand a plain-text
2172
2229
  # answer. Without this the model happily emits one more tool_calls
@@ -2224,6 +2281,7 @@ module PWN
2224
2281
  msg = call_engine(messages: messages, tools: last_iter ? nil : tools, ts_state: ts_state)
2225
2282
  if msg.nil?
2226
2283
  task_summary_flush!(state: ts_state, on_tool: on_tool)
2284
+ quiet_debug_tui!(reason: 'engine_empty')
2227
2285
  return '[pwn-ai] engine returned no message'
2228
2286
  end
2229
2287
 
@@ -2293,6 +2351,8 @@ module PWN
2293
2351
  }
2294
2352
  next
2295
2353
  end
2354
+ debug_progress(msg: "final accepted chars=#{text.to_s.length}")
2355
+ quiet_debug_tui!(reason: 'final')
2296
2356
  append_session(session_id: session_id, role: 'assistant', content: text)
2297
2357
  Learning.auto_introspect(session_id: session_id, request: request, final: text, predicted: predicted, plan: ts_state && ts_state[:plan], ts_state: ts_state) if defined?(Learning) && should_auto_introspect?(local: local, turn_fails: turn_fails, iter: i)
2298
2358
  maybe_finish_policy(session_id: session_id, proxy_ok: true, ts_state: ts_state)
@@ -2329,6 +2389,7 @@ module PWN
2329
2389
  else
2330
2390
  raw = Dispatch.call(tool_call: tc)
2331
2391
  end
2392
+ debug_progress(msg: "dispatch #{name} ok_len=#{raw.to_s.length}")
2332
2393
  tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
2333
2394
  result = Result.condition(content: raw, entry: entry)
2334
2395
 
@@ -2385,6 +2446,8 @@ module PWN
2385
2446
  # failure (previously we only Mistakes.record'd and returned a bare
2386
2447
  # string — no session row, no judge, no hindsight).
2387
2448
  final_msg = '[pwn-ai] iteration budget exhausted'
2449
+ debug_progress(msg: 'final budget_exhausted')
2450
+ quiet_debug_tui!(reason: 'budget_exhausted')
2388
2451
  if defined?(Mistakes)
2389
2452
  Mistakes.record(
2390
2453
  tool: 'agent_loop',
@@ -117,6 +117,9 @@ module PWN
117
117
  RECENT TURNS block or one memory_recall — never a multi-tool plan.
118
118
  Pure greetings / light smalltalk short-circuit to a fixed ack — never
119
119
  echo weather or invent social filler ("noted, cloudy out there").
120
+ World-knowledge questions (what color is X) may be answered as a
121
+ text final with no tools. Tools stay available; do not invent a
122
+ host-work plan for them.
120
123
  Do not treat process_sop_* or operator_pref_* memory about code
121
124
  hygiene as the current user goal unless they asked to change code.
122
125
  PROMPT
@@ -490,122 +490,6 @@ module PWN
490
490
  raise e
491
491
  end
492
492
 
493
- # Supported Method Parameters::
494
- # usage = PWN::AI::Anthropic.get_plan_usage(
495
- # timeout: 'optional - seconds (default 8)'
496
- # )
497
- #
498
- # Admin Usage API (organizations/usage_report/messages) yields tokens
499
- # used; organizations/rate_limits yields the configured ceiling so the
500
- # PS1 can show a percent. Individual accounts without an Admin key
501
- # return {available:false}. Never prompts for credentials.
502
- public_class_method def self.get_plan_usage(opts = {})
503
- timeout = opts[:timeout] || 8
504
- return { available: false, engine: :anthropic } unless plan_usage_credentials?
505
-
506
- now = Time.now.utc
507
- start_of_month = Time.utc(now.year, now.month, 1)
508
- report = parse_plan_usage_json(
509
- raw: anthropic_rest_call(
510
- rest_call: 'organizations/usage_report/messages',
511
- params: {
512
- starting_at: start_of_month.strftime('%Y-%m-%dT00:00:00Z'),
513
- ending_at: now.strftime('%Y-%m-%dT%H:%M:%SZ'),
514
- bucket_width: '1d'
515
- },
516
- timeout: timeout,
517
- spinner: false,
518
- non_interactive: true
519
- )
520
- )
521
- used_tokens = sum_usage_tokens(report: report)
522
-
523
- limits = parse_plan_usage_json(
524
- raw: anthropic_rest_call(
525
- rest_call: 'organizations/rate_limits',
526
- timeout: timeout,
527
- spinner: false,
528
- non_interactive: true
529
- )
530
- )
531
- limit_tokens = first_rate_limit(limits: limits)
532
-
533
- normalized = PWN::AI.normalize_plan_usage(
534
- used: used_tokens,
535
- limit: limit_tokens,
536
- source: 'organizations/usage_report',
537
- engine: :anthropic
538
- )
539
- return normalized if normalized[:available]
540
-
541
- { available: false, engine: :anthropic }
542
- rescue StandardError
543
- { available: false, engine: :anthropic }
544
- end
545
-
546
- private_class_method def self.plan_usage_credentials?
547
- engine = PWN::Env.dig(:ai, :anthropic) || {}
548
- oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
549
- real_config_value?(value: engine[:key]) ||
550
- real_config_value?(value: oauth[:bearer_token]) ||
551
- real_config_value?(value: oauth[:refresh_token])
552
- end
553
-
554
- private_class_method def self.parse_plan_usage_json(opts = {})
555
- raw = opts[:raw]
556
- return nil if raw.nil?
557
-
558
- parsed = JSON.parse(raw.to_s, symbolize_names: true)
559
- parsed.is_a?(Hash) ? parsed : nil
560
- rescue JSON::ParserError, TypeError
561
- nil
562
- end
563
-
564
- private_class_method def self.sum_usage_tokens(opts = {})
565
- report = opts[:report]
566
- return nil unless report.is_a?(Hash)
567
-
568
- buckets = report[:data] || report[:usage] || []
569
- total = 0
570
- Array(buckets).each do |bucket|
571
- next unless bucket.is_a?(Hash)
572
-
573
- results = bucket[:results] || bucket[:usage] || [bucket]
574
- Array(results).each do |row|
575
- next unless row.is_a?(Hash)
576
-
577
- %i[
578
- uncached_input_tokens
579
- input_tokens
580
- cache_read_input_tokens
581
- cache_creation_input_tokens
582
- output_tokens
583
- ].each { |k| total += row[k].to_i }
584
- end
585
- end
586
- total.positive? ? total : nil
587
- end
588
-
589
- private_class_method def self.first_rate_limit(opts = {})
590
- limits = opts[:limits]
591
- return nil unless limits.is_a?(Hash)
592
-
593
- Array(limits[:data]).each do |group|
594
- next unless group.is_a?(Hash)
595
-
596
- Array(group[:limits]).each do |lim|
597
- next unless lim.is_a?(Hash)
598
-
599
- type = lim[:type].to_s
600
- next unless type.match?(/tokens_per|input_tokens|output_tokens/)
601
-
602
- v = lim[:value].to_f
603
- return v if v.positive?
604
- end
605
- end
606
- nil
607
- end
608
-
609
493
  # ----------------------------------------------------------------------
610
494
  # Native tool-calling adapter for PWN::AI::Agent::Loop.
611
495
  #
@@ -960,8 +844,6 @@ module PWN
960
844
  puts "USAGE:
961
845
  models = #{self}.get_models
962
846
 
963
- usage = #{self}.get_plan_usage
964
-
965
847
  # One-time Claude Pro/Max OAuth enrollment (PKCE paste flow):
966
848
  bearer = #{self}.obtain_oauth_bearer_token
967
849
  # Subsequent runs silently refresh via ai.anthropic.oauth.refresh_token
data/lib/pwn/ai/gemini.rb CHANGED
@@ -114,19 +114,6 @@ module PWN
114
114
  raise e
115
115
  end
116
116
 
117
- # Supported Method Parameters::
118
- # usage = PWN::AI::Gemini.get_plan_usage(
119
- # timeout: 'optional - seconds (default 8)'
120
- # )
121
- #
122
- # Gemini Generative Language has no subscription-usage endpoint that
123
- # returns used-vs-limit for an API key. Cached-miss / unavailable so
124
- # the PS1 shows ∞ unless the operator later injects used/limit.
125
- public_class_method def self.get_plan_usage(opts = {})
126
- _timeout = opts[:timeout] || 8
127
- { available: false, engine: :gemini }
128
- end
129
-
130
117
  # ----------------------------------------------------------------------
131
118
  # Native tool-calling adapter for PWN::AI::Agent::Loop.
132
119
  #
@@ -449,8 +436,6 @@ module PWN
449
436
  puts "USAGE:
450
437
  models = #{self}.get_models
451
438
 
452
- usage = #{self}.get_plan_usage
453
-
454
439
  response = #{self}.chat(
455
440
  request: 'required - message to Gemini',
456
441
  model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:gemini][:model])',
data/lib/pwn/ai/grok.rb CHANGED
@@ -505,162 +505,6 @@ module PWN
505
505
  raise e
506
506
  end
507
507
 
508
- # Supported Method Parameters::
509
- # usage = PWN::AI::Grok.get_plan_usage(
510
- # timeout: 'optional - seconds (default 8)'
511
- # )
512
- #
513
- # Subscription / team usage for the PS1 percent suffix.
514
- # GET /v1/api-key yields team_id; then billing preview /
515
- # spending-limits / prepaid balance compute used vs limit.
516
- # GET /v1/me is also tried (OAuth personal-team payload).
517
- public_class_method def self.get_plan_usage(opts = {})
518
- timeout = opts[:timeout] || 8
519
- return { available: false, engine: :grok } unless plan_usage_credentials?
520
-
521
- me = parse_plan_usage_json(
522
- raw: grok_rest_call(rest_call: 'me', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
523
- )
524
- if me.is_a?(Hash)
525
- used = first_numeric(src: me, keys: %i[used usage spend spent used_usd usage_usd])
526
- limit = first_numeric(src: me, keys: %i[limit quota allowance hard_limit spending_limit])
527
- normalized = PWN::AI.normalize_plan_usage(
528
- used: used,
529
- limit: limit,
530
- source: 'me',
531
- engine: :grok
532
- )
533
- return normalized if normalized[:available]
534
- end
535
-
536
- key_info = parse_plan_usage_json(
537
- raw: grok_rest_call(rest_call: 'api-key', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
538
- )
539
- team_id = key_info.is_a?(Hash) ? key_info[:team_id] : nil
540
- team_id ||= me.is_a?(Hash) ? (me[:team_id] || me.dig(:team, :id)) : nil
541
-
542
- if team_id.to_s.strip != ''
543
- preview = parse_plan_usage_json(
544
- raw: grok_rest_call(
545
- rest_call: "billing/teams/#{team_id}/postpaid/invoice/preview",
546
- timeout: timeout,
547
- spinner: false,
548
- quiet: true,
549
- non_interactive: true
550
- )
551
- )
552
- limits = parse_plan_usage_json(
553
- raw: grok_rest_call(
554
- rest_call: "billing/teams/#{team_id}/postpaid/spending-limits",
555
- timeout: timeout,
556
- spinner: false,
557
- quiet: true,
558
- non_interactive: true
559
- )
560
- )
561
- balance = parse_plan_usage_json(
562
- raw: grok_rest_call(
563
- rest_call: "billing/teams/#{team_id}/prepaid/balance",
564
- timeout: timeout,
565
- spinner: false,
566
- quiet: true,
567
- non_interactive: true
568
- )
569
- )
570
-
571
- used = nil
572
- limit = nil
573
- if preview.is_a?(Hash)
574
- core = preview[:coreInvoice] || preview[:core_invoice] || {}
575
- used_cents = cents_val(src: core, key: :prepaidCreditsUsed) ||
576
- cents_val(src: core, key: :prepaid_credits_used) ||
577
- core[:amountAfterVat] ||
578
- core[:amount_after_vat]
579
- prepaid_cents = cents_val(src: core, key: :prepaidCredits) ||
580
- cents_val(src: core, key: :prepaid_credits)
581
- spend_limit_cents = preview[:effectiveSpendingLimit] || preview[:effective_spending_limit]
582
- used = to_f_or_nil(value: used_cents)
583
- limit = to_f_or_nil(value: spend_limit_cents)
584
- limit = to_f_or_nil(value: prepaid_cents).abs if (limit.nil? || limit <= 0) && prepaid_cents
585
- end
586
- used = to_f_or_nil(value: cents_val(src: balance, key: :total)).to_f.abs if (used.nil? || used <= 0) && balance.is_a?(Hash)
587
- if (limit.nil? || limit <= 0) && limits.is_a?(Hash)
588
- sl = limits[:spendingLimits] || limits[:spending_limits] || {}
589
- limit = to_f_or_nil(value: cents_val(src: sl, key: :effectiveSl)) ||
590
- to_f_or_nil(value: cents_val(src: sl, key: :effectiveHardSl)) ||
591
- to_f_or_nil(value: cents_val(src: sl, key: :softSl))
592
- end
593
-
594
- normalized = PWN::AI.normalize_plan_usage(
595
- used: used,
596
- limit: limit,
597
- source: 'billing/teams',
598
- engine: :grok
599
- )
600
- return normalized if normalized[:available]
601
- end
602
-
603
- { available: false, engine: :grok }
604
- rescue StandardError
605
- { available: false, engine: :grok }
606
- end
607
-
608
- private_class_method def self.plan_usage_credentials?
609
- engine = PWN::Env.dig(:ai, :grok) || {}
610
- oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
611
- real_config_value?(value: engine[:key]) ||
612
- real_config_value?(value: oauth[:bearer_token]) ||
613
- real_config_value?(value: oauth[:refresh_token])
614
- end
615
-
616
- private_class_method def self.parse_plan_usage_json(opts = {})
617
- raw = opts[:raw]
618
- return nil if raw.nil?
619
-
620
- parsed = JSON.parse(raw.to_s, symbolize_names: true)
621
- parsed.is_a?(Hash) ? parsed : nil
622
- rescue JSON::ParserError, TypeError
623
- nil
624
- end
625
-
626
- private_class_method def self.cents_val(opts = {})
627
- src = opts[:src]
628
- key = opts[:key]
629
- return nil unless src.is_a?(Hash)
630
-
631
- v = src[key]
632
- return v[:val] if v.is_a?(Hash) && v.key?(:val)
633
- return v['val'] if v.is_a?(Hash) && v.key?('val')
634
-
635
- v
636
- end
637
-
638
- private_class_method def self.to_f_or_nil(opts = {})
639
- val = opts.is_a?(Hash) && opts.key?(:value) ? opts[:value] : opts
640
- return nil if val.nil?
641
- return val.to_f if val.respond_to?(:to_f)
642
-
643
- nil
644
- rescue StandardError
645
- nil
646
- end
647
-
648
- private_class_method def self.first_numeric(opts = {})
649
- src = opts[:src]
650
- keys = Array(opts[:keys])
651
- return nil unless src.is_a?(Hash)
652
-
653
- keys.each do |k|
654
- v = src[k]
655
- next if v.nil?
656
- next if v.is_a?(Hash)
657
-
658
- f = v.to_f
659
- return f if f.positive? || v.to_s.strip.match?(/\A-?\d/)
660
- end
661
- nil
662
- end
663
-
664
508
  # Supported Method Parameters::
665
509
  # response = PWN::AI::Grok.chat_with_tools(
666
510
  # messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
@@ -845,8 +689,6 @@ module PWN
845
689
  puts "USAGE:
846
690
  models = #{self}.get_models
847
691
 
848
- usage = #{self}.get_plan_usage
849
-
850
692
  response = #{self}.chat(
851
693
  request: 'required - message to Grok',
852
694
  model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:grok][:model])',
data/lib/pwn/ai/ollama.rb CHANGED
@@ -370,16 +370,6 @@ module PWN
370
370
  raise e
371
371
  end
372
372
 
373
- # Supported Method Parameters::
374
- # usage = PWN::AI::Ollama.get_plan_usage
375
- #
376
- # Local Ollama has no subscription / plan-usage endpoint. Always
377
- # unavailable so the PS1 shows the infinity glyph.
378
- public_class_method def self.get_plan_usage(opts = {})
379
- _unused = opts
380
- { available: false, engine: :ollama, unlimited: true }
381
- end
382
-
383
373
  # Coerce OpenAI-wire message history into Ollama-native shapes before
384
374
  # POST /api/chat (or Open WebUI /ollama/api/chat):
385
375
  # - function.arguments must be a Hash/Array object, not a JSON string
@@ -652,8 +642,6 @@ module PWN
652
642
  puts "USAGE:
653
643
  models = #{self}.get_models
654
644
 
655
- usage = #{self}.get_plan_usage
656
-
657
645
  response = #{self}.chat(
658
646
  request: 'required - message to Ollama',
659
647
  model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:ollama][:model])',