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 +4 -4
- data/documentation/General-PWN-Usage.md +1 -1
- data/documentation/pwn-REPL.md +1 -1
- data/lib/pwn/ai/agent/loop.rb +76 -13
- data/lib/pwn/ai/agent/prompt_builder.rb +3 -0
- data/lib/pwn/ai/anthropic.rb +0 -118
- data/lib/pwn/ai/gemini.rb +0 -15
- data/lib/pwn/ai/grok.rb +0 -158
- data/lib/pwn/ai/ollama.rb +0 -12
- data/lib/pwn/ai/open_ai.rb +0 -110
- data/lib/pwn/ai/open_web_ui.rb +0 -12
- data/lib/pwn/ai.rb +0 -86
- data/lib/pwn/plugins/log.rb +302 -0
- data/lib/pwn/plugins/repl.rb +28 -8
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +43 -5
- data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
- data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
- data/spec/lib/pwn/ai/grok_spec.rb +2 -2
- data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
- data/spec/lib/pwn/ai_spec.rb +4 -21
- data/spec/lib/pwn/plugins/log_spec.rb +139 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +31 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
- data/third_party/pwn_rdoc.jsonl +26 -20
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c85ad9f56d45556168111931b475458009ad82fd7dac279457824484dde1105
|
|
4
|
+
data.tar.gz: 8cc423564b1f6adc95674b7002ca2a33abcc3e337b0d9a4dbf493a108431d1af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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` |
|
|
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 |
|
data/documentation/pwn-REPL.md
CHANGED
|
@@ -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` |
|
|
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 |
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
2092
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -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])',
|