pwn 0.5.674 → 0.5.675
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/Gemfile +1 -1
- data/bin/pwn +2 -0
- data/bin/pwn_setup +25 -2
- data/lib/pwn/ai/agent/loop.rb +10 -0
- data/lib/pwn/ai/agent/reflect.rb +3 -1
- data/lib/pwn/ai/agent/reward.rb +27 -3
- data/lib/pwn/ai/agent/task_summarizer.rb +20 -3
- data/lib/pwn/ai/anthropic.rb +128 -4
- data/lib/pwn/ai/gemini.rb +15 -0
- data/lib/pwn/ai/grok.rb +185 -9
- data/lib/pwn/ai/ollama.rb +30 -5
- data/lib/pwn/ai/open_ai.rb +136 -9
- data/lib/pwn/ai/open_web_ui.rb +12 -0
- data/lib/pwn/ai.rb +86 -0
- data/lib/pwn/cron.rb +428 -15
- data/lib/pwn/plugins/repl.rb +3 -2
- data/lib/pwn/setup.rb +70 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/documentation/installation_md_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +7 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +6 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +4 -0
- data/spec/lib/pwn/ai/gemini_spec.rb +4 -0
- data/spec/lib/pwn/ai/grok_spec.rb +14 -0
- data/spec/lib/pwn/ai/ollama_spec.rb +30 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +33 -0
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +9 -0
- data/spec/lib/pwn/ai_spec.rb +23 -0
- data/spec/lib/pwn/cron_spec.rb +143 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +12 -0
- data/spec/lib/pwn/setup_spec.rb +43 -0
- data/spec/support/sandbox.rb +4 -0
- data/third_party/pwn_rdoc.jsonl +48 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ed5f171854df678e76d477ad52fee2dd42e1687afc6e950281d2b3e5fe75ab9
|
|
4
|
+
data.tar.gz: 35712ab0300a98de2dfc52430278088561b84fcd15e4dd70a3c95cd90ccf4645
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 563ee1d396728ef91353cfc4e6d82da6b90662d03e55d1c027319203e47a8bd91d9c723a09cda3a6836dc9b1ed380bb3e0bb76d802dd23157ff7c92d52a3958b
|
|
7
|
+
data.tar.gz: 05e3c313c8262273f5799f24a9359b3cadc5448ccebfc5ae031a99a71865b548d24489208f4b7be6e0d4790485ebcc58d35a3ccdafbfddfecdb27eaddd531b9c
|
data/Gemfile
CHANGED
data/bin/pwn
CHANGED
|
@@ -51,9 +51,11 @@ begin
|
|
|
51
51
|
# ------------------------------------------------------------------
|
|
52
52
|
if opts[:setup] == true
|
|
53
53
|
r = PWN::Setup.check
|
|
54
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
54
55
|
exit(r[:ok] ? 0 : 1)
|
|
55
56
|
else
|
|
56
57
|
PWN::Setup.deps(profile: opts[:setup])
|
|
58
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
57
59
|
end
|
|
58
60
|
elsif opts[:ai]
|
|
59
61
|
# ------------------------------------------------------------------
|
data/bin/pwn_setup
CHANGED
|
@@ -59,6 +59,18 @@ pwn_driver.auto_opts_help = false
|
|
|
59
59
|
pwn_driver.parse!
|
|
60
60
|
|
|
61
61
|
begin
|
|
62
|
+
status = 0
|
|
63
|
+
# Seed default jobs + start/reuse the in-process scheduler so last_run
|
|
64
|
+
# actually advances. Do this BEFORE the doctor so `pwn setup` reports
|
|
65
|
+
# the worker as running. Skip list/terminal (no host scheduler needed)
|
|
66
|
+
# and dry-run (must not spawn).
|
|
67
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
68
|
+
PWN::Setup.ensure_cron(
|
|
69
|
+
restart: false,
|
|
70
|
+
dry_run: false
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
if opts[:list]
|
|
63
75
|
PWN::Setup.list_profiles
|
|
64
76
|
elsif opts[:migrate]
|
|
@@ -66,7 +78,7 @@ begin
|
|
|
66
78
|
fix: opts[:fix] || opts[:yes],
|
|
67
79
|
dry_run: opts[:dry_run]
|
|
68
80
|
)
|
|
69
|
-
|
|
81
|
+
status = 1 unless r && r[:after] && r[:after][:incompatible].empty?
|
|
70
82
|
elsif opts[:terminal]
|
|
71
83
|
PWN::Setup.terminal(
|
|
72
84
|
yes: opts[:yes],
|
|
@@ -83,8 +95,19 @@ begin
|
|
|
83
95
|
# `pwn setup --check` → same
|
|
84
96
|
# `pwn setup --check --profile net` → gate ONLY on the :net profile
|
|
85
97
|
r = PWN::Setup.check(profile: opts[:profile])
|
|
86
|
-
|
|
98
|
+
status = 1 unless r[:ok]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Seed default jobs + start/reuse the in-process scheduler so
|
|
102
|
+
# last_run actually advances. Skip list/terminal (no host scheduler
|
|
103
|
+
# needed) and dry-run (must not spawn).
|
|
104
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
105
|
+
PWN::Setup.ensure_cron(
|
|
106
|
+
restart: false,
|
|
107
|
+
dry_run: false
|
|
108
|
+
)
|
|
87
109
|
end
|
|
110
|
+
exit status
|
|
88
111
|
rescue Interrupt
|
|
89
112
|
puts "\naborted."
|
|
90
113
|
exit 130
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1063,6 +1063,16 @@ module PWN
|
|
|
1063
1063
|
# 3.2 — local models cannot afford auto_introspect (judge+prm+critic+
|
|
1064
1064
|
# sentinel+extro) on every success. Default :failure_only when local.
|
|
1065
1065
|
private_class_method def self.should_auto_introspect?(opts = {})
|
|
1066
|
+
kind = (opts[:kind] || Thread.current[:pwn_request_kind]).to_s.to_sym
|
|
1067
|
+
intent = (opts[:intent] || Thread.current[:pwn_request_intent]).to_s.to_sym
|
|
1068
|
+
fails = opts[:turn_fails].is_a?(Hash) ? opts[:turn_fails].values.sum : 0
|
|
1069
|
+
# Cheap answers already returned user-visible text. The post-answer
|
|
1070
|
+
# critic + 12s ORM printed ERROR: Timed out reading data from server
|
|
1071
|
+
# after greetings / takes / questions.
|
|
1072
|
+
return false if %i[greeting howto recall].include?(intent)
|
|
1073
|
+
return false if kind == :statement
|
|
1074
|
+
return false if kind == :question && fails.zero?
|
|
1075
|
+
|
|
1066
1076
|
return true unless opts[:local]
|
|
1067
1077
|
|
|
1068
1078
|
policy = agent_flag(key: :local_introspect, default: :failure_only).to_s.to_sym
|
data/lib/pwn/ai/agent/reflect.rb
CHANGED
|
@@ -97,7 +97,8 @@ module PWN
|
|
|
97
97
|
system_role_content: system_role_content,
|
|
98
98
|
spinner: spinner,
|
|
99
99
|
timeout: timeout,
|
|
100
|
-
temp: temp
|
|
100
|
+
temp: temp,
|
|
101
|
+
quiet: opts[:quiet]
|
|
101
102
|
)
|
|
102
103
|
end
|
|
103
104
|
ensure
|
|
@@ -132,6 +133,7 @@ module PWN
|
|
|
132
133
|
chat_opts[:timeout] = opts[:timeout] unless opts[:timeout].nil?
|
|
133
134
|
chat_opts[:temp] = opts[:temp] unless opts[:temp].nil?
|
|
134
135
|
chat_opts[:model] = opts[:model] unless opts[:model].to_s.empty?
|
|
136
|
+
chat_opts[:quiet] = opts[:quiet] unless opts[:quiet].nil?
|
|
135
137
|
mod.chat(chat_opts)
|
|
136
138
|
end
|
|
137
139
|
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -1157,11 +1157,20 @@ module PWN
|
|
|
1157
1157
|
spinner: false,
|
|
1158
1158
|
model: judge_model,
|
|
1159
1159
|
timeout: cheap_orm_timeout,
|
|
1160
|
-
temp: CHEAP_ORM_TEMP
|
|
1160
|
+
temp: CHEAP_ORM_TEMP,
|
|
1161
|
+
quiet: true
|
|
1161
1162
|
)
|
|
1162
1163
|
text = extract_chat_text(resp: raw)
|
|
1163
1164
|
return text unless text.strip.empty?
|
|
1164
|
-
|
|
1165
|
+
|
|
1166
|
+
# Quiet ReadTimeout now returns nil instead of raising. That
|
|
1167
|
+
# empty response already spent the cheap-ORM budget — do not
|
|
1168
|
+
# immediately fire engine_chat_cheap (second 12s stall).
|
|
1169
|
+
return nil
|
|
1170
|
+
rescue StandardError => e
|
|
1171
|
+
# A 12s ReadTimeout already spent the cheap-ORM budget.
|
|
1172
|
+
return nil if timeout_error?(err: e)
|
|
1173
|
+
|
|
1165
1174
|
nil
|
|
1166
1175
|
end
|
|
1167
1176
|
end
|
|
@@ -1191,7 +1200,8 @@ module PWN
|
|
|
1191
1200
|
system_role_content: opts[:system_role_content],
|
|
1192
1201
|
spinner: false,
|
|
1193
1202
|
timeout: cheap_orm_timeout,
|
|
1194
|
-
temp: CHEAP_ORM_TEMP
|
|
1203
|
+
temp: CHEAP_ORM_TEMP,
|
|
1204
|
+
quiet: true
|
|
1195
1205
|
}
|
|
1196
1206
|
model = judge_model
|
|
1197
1207
|
chat_opts[:model] = model unless model.to_s.empty?
|
|
@@ -1200,6 +1210,20 @@ module PWN
|
|
|
1200
1210
|
nil
|
|
1201
1211
|
end
|
|
1202
1212
|
|
|
1213
|
+
private_class_method def self.timeout_error?(opts = {})
|
|
1214
|
+
err = opts[:err]
|
|
1215
|
+
return false if err.nil?
|
|
1216
|
+
|
|
1217
|
+
klass = err.class
|
|
1218
|
+
name = klass.name.to_s
|
|
1219
|
+
return true if name.include?('Timeout') || name.include?('ReadTimeout') || name.include?('OpenTimeout')
|
|
1220
|
+
return true if err.message.to_s.match?(/timed out reading|read timeout|open timeout/i)
|
|
1221
|
+
|
|
1222
|
+
false
|
|
1223
|
+
rescue StandardError
|
|
1224
|
+
false
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1203
1227
|
private_class_method def self.cheap_orm_timeout
|
|
1204
1228
|
n = agent_flag(key: :reward_llm_timeout, default: CHEAP_ORM_TIMEOUT).to_i
|
|
1205
1229
|
n = CHEAP_ORM_TIMEOUT if n < 2
|
|
@@ -566,7 +566,9 @@ module PWN
|
|
|
566
566
|
request: user,
|
|
567
567
|
system_role_content: system,
|
|
568
568
|
suppress_pii_warning: true,
|
|
569
|
-
spinner: false
|
|
569
|
+
spinner: false,
|
|
570
|
+
timeout: sidecar_timeout,
|
|
571
|
+
quiet: true
|
|
570
572
|
)
|
|
571
573
|
text = reflect_text(resp: resp)
|
|
572
574
|
return text unless text.to_s.strip.empty?
|
|
@@ -846,7 +848,9 @@ module PWN
|
|
|
846
848
|
request: user,
|
|
847
849
|
system_role_content: system,
|
|
848
850
|
suppress_pii_warning: true,
|
|
849
|
-
spinner: false
|
|
851
|
+
spinner: false,
|
|
852
|
+
timeout: sidecar_timeout,
|
|
853
|
+
quiet: true
|
|
850
854
|
)
|
|
851
855
|
text = reflect_text(resp: resp)
|
|
852
856
|
return text unless text.to_s.strip.empty?
|
|
@@ -896,7 +900,9 @@ module PWN
|
|
|
896
900
|
r = mod.chat(
|
|
897
901
|
request: opts[:request],
|
|
898
902
|
system_role_content: opts[:system_role_content],
|
|
899
|
-
spinner: false
|
|
903
|
+
spinner: false,
|
|
904
|
+
timeout: sidecar_timeout,
|
|
905
|
+
quiet: true
|
|
900
906
|
)
|
|
901
907
|
reflect_text(resp: r)
|
|
902
908
|
rescue StandardError => e
|
|
@@ -911,6 +917,17 @@ module PWN
|
|
|
911
917
|
:ollama
|
|
912
918
|
end
|
|
913
919
|
|
|
920
|
+
# Kind / plan classification is a sidecar hop. Bound it so a hung
|
|
921
|
+
# model cannot stall the user's real answer, and stay quiet on timeout.
|
|
922
|
+
private_class_method def self.sidecar_timeout
|
|
923
|
+
n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
|
|
924
|
+
n = 20 if n < 5
|
|
925
|
+
n = 60 if n > 60
|
|
926
|
+
n
|
|
927
|
+
rescue StandardError
|
|
928
|
+
20
|
|
929
|
+
end
|
|
930
|
+
|
|
914
931
|
# Parse JSON array, fenced JSON, or numbered/bulleted plain text.
|
|
915
932
|
#
|
|
916
933
|
# Supported Method Parameters::
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -362,7 +362,7 @@ module PWN
|
|
|
362
362
|
real_config_value?(value: oauth[:bearer_token]) ||
|
|
363
363
|
real_config_value?(value: oauth[:refresh_token])
|
|
364
364
|
|
|
365
|
-
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key]))
|
|
365
|
+
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key])) && !opts[:non_interactive]
|
|
366
366
|
token = obtain_oauth_bearer_token(oauth)
|
|
367
367
|
using_oauth = true
|
|
368
368
|
end
|
|
@@ -372,9 +372,15 @@ module PWN
|
|
|
372
372
|
using_oauth = false
|
|
373
373
|
end
|
|
374
374
|
|
|
375
|
-
token
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
if token.nil?
|
|
376
|
+
return nil if opts[:non_interactive]
|
|
377
|
+
|
|
378
|
+
token = PWN::Plugins::AuthenticationHelper.mask_password(
|
|
379
|
+
prompt: 'Anthropic API Key (or run PWN::AI::Anthropic.obtain_oauth_bearer_token for Claude Pro/Max OAuth)'
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
return nil if token.nil?
|
|
378
384
|
|
|
379
385
|
http_method = if opts[:http_method].nil?
|
|
380
386
|
:get
|
|
@@ -488,6 +494,122 @@ module PWN
|
|
|
488
494
|
raise e
|
|
489
495
|
end
|
|
490
496
|
|
|
497
|
+
# Supported Method Parameters::
|
|
498
|
+
# usage = PWN::AI::Anthropic.get_plan_usage(
|
|
499
|
+
# timeout: 'optional - seconds (default 8)'
|
|
500
|
+
# )
|
|
501
|
+
#
|
|
502
|
+
# Admin Usage API (organizations/usage_report/messages) yields tokens
|
|
503
|
+
# used; organizations/rate_limits yields the configured ceiling so the
|
|
504
|
+
# PS1 can show a percent. Individual accounts without an Admin key
|
|
505
|
+
# return {available:false}. Never prompts for credentials.
|
|
506
|
+
public_class_method def self.get_plan_usage(opts = {})
|
|
507
|
+
timeout = opts[:timeout] || 8
|
|
508
|
+
return { available: false, engine: :anthropic } unless plan_usage_credentials?
|
|
509
|
+
|
|
510
|
+
now = Time.now.utc
|
|
511
|
+
start_of_month = Time.utc(now.year, now.month, 1)
|
|
512
|
+
report = parse_plan_usage_json(
|
|
513
|
+
raw: anthropic_rest_call(
|
|
514
|
+
rest_call: 'organizations/usage_report/messages',
|
|
515
|
+
params: {
|
|
516
|
+
starting_at: start_of_month.strftime('%Y-%m-%dT00:00:00Z'),
|
|
517
|
+
ending_at: now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
518
|
+
bucket_width: '1d'
|
|
519
|
+
},
|
|
520
|
+
timeout: timeout,
|
|
521
|
+
spinner: false,
|
|
522
|
+
non_interactive: true
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
used_tokens = sum_usage_tokens(report: report)
|
|
526
|
+
|
|
527
|
+
limits = parse_plan_usage_json(
|
|
528
|
+
raw: anthropic_rest_call(
|
|
529
|
+
rest_call: 'organizations/rate_limits',
|
|
530
|
+
timeout: timeout,
|
|
531
|
+
spinner: false,
|
|
532
|
+
non_interactive: true
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
limit_tokens = first_rate_limit(limits: limits)
|
|
536
|
+
|
|
537
|
+
normalized = PWN::AI.normalize_plan_usage(
|
|
538
|
+
used: used_tokens,
|
|
539
|
+
limit: limit_tokens,
|
|
540
|
+
source: 'organizations/usage_report',
|
|
541
|
+
engine: :anthropic
|
|
542
|
+
)
|
|
543
|
+
return normalized if normalized[:available]
|
|
544
|
+
|
|
545
|
+
{ available: false, engine: :anthropic }
|
|
546
|
+
rescue StandardError
|
|
547
|
+
{ available: false, engine: :anthropic }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
private_class_method def self.plan_usage_credentials?
|
|
551
|
+
engine = PWN::Env.dig(:ai, :anthropic) || {}
|
|
552
|
+
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
553
|
+
real_config_value?(value: engine[:key]) ||
|
|
554
|
+
real_config_value?(value: oauth[:bearer_token]) ||
|
|
555
|
+
real_config_value?(value: oauth[:refresh_token])
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
559
|
+
raw = opts[:raw]
|
|
560
|
+
return nil if raw.nil?
|
|
561
|
+
|
|
562
|
+
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
563
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
564
|
+
rescue JSON::ParserError, TypeError
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
private_class_method def self.sum_usage_tokens(opts = {})
|
|
569
|
+
report = opts[:report]
|
|
570
|
+
return nil unless report.is_a?(Hash)
|
|
571
|
+
|
|
572
|
+
buckets = report[:data] || report[:usage] || []
|
|
573
|
+
total = 0
|
|
574
|
+
Array(buckets).each do |bucket|
|
|
575
|
+
next unless bucket.is_a?(Hash)
|
|
576
|
+
|
|
577
|
+
results = bucket[:results] || bucket[:usage] || [bucket]
|
|
578
|
+
Array(results).each do |row|
|
|
579
|
+
next unless row.is_a?(Hash)
|
|
580
|
+
|
|
581
|
+
%i[
|
|
582
|
+
uncached_input_tokens
|
|
583
|
+
input_tokens
|
|
584
|
+
cache_read_input_tokens
|
|
585
|
+
cache_creation_input_tokens
|
|
586
|
+
output_tokens
|
|
587
|
+
].each { |k| total += row[k].to_i }
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
total.positive? ? total : nil
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
private_class_method def self.first_rate_limit(opts = {})
|
|
594
|
+
limits = opts[:limits]
|
|
595
|
+
return nil unless limits.is_a?(Hash)
|
|
596
|
+
|
|
597
|
+
Array(limits[:data]).each do |group|
|
|
598
|
+
next unless group.is_a?(Hash)
|
|
599
|
+
|
|
600
|
+
Array(group[:limits]).each do |lim|
|
|
601
|
+
next unless lim.is_a?(Hash)
|
|
602
|
+
|
|
603
|
+
type = lim[:type].to_s
|
|
604
|
+
next unless type.match?(/tokens_per|input_tokens|output_tokens/)
|
|
605
|
+
|
|
606
|
+
v = lim[:value].to_f
|
|
607
|
+
return v if v.positive?
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
491
613
|
# ----------------------------------------------------------------------
|
|
492
614
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
493
615
|
#
|
|
@@ -832,6 +954,8 @@ module PWN
|
|
|
832
954
|
puts "USAGE:
|
|
833
955
|
models = #{self}.get_models
|
|
834
956
|
|
|
957
|
+
usage = #{self}.get_plan_usage
|
|
958
|
+
|
|
835
959
|
# One-time Claude Pro/Max OAuth enrollment (PKCE paste flow):
|
|
836
960
|
bearer = #{self}.obtain_oauth_bearer_token
|
|
837
961
|
# Subsequent runs silently refresh via ai.anthropic.oauth.refresh_token
|
data/lib/pwn/ai/gemini.rb
CHANGED
|
@@ -118,6 +118,19 @@ module PWN
|
|
|
118
118
|
raise e
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# Supported Method Parameters::
|
|
122
|
+
# usage = PWN::AI::Gemini.get_plan_usage(
|
|
123
|
+
# timeout: 'optional - seconds (default 8)'
|
|
124
|
+
# )
|
|
125
|
+
#
|
|
126
|
+
# Gemini Generative Language has no subscription-usage endpoint that
|
|
127
|
+
# returns used-vs-limit for an API key. Cached-miss / unavailable so
|
|
128
|
+
# the PS1 shows ∞ unless the operator later injects used/limit.
|
|
129
|
+
public_class_method def self.get_plan_usage(opts = {})
|
|
130
|
+
_timeout = opts[:timeout] || 8
|
|
131
|
+
{ available: false, engine: :gemini }
|
|
132
|
+
end
|
|
133
|
+
|
|
121
134
|
# ----------------------------------------------------------------------
|
|
122
135
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
123
136
|
#
|
|
@@ -432,6 +445,8 @@ module PWN
|
|
|
432
445
|
puts "USAGE:
|
|
433
446
|
models = #{self}.get_models
|
|
434
447
|
|
|
448
|
+
usage = #{self}.get_plan_usage
|
|
449
|
+
|
|
435
450
|
response = #{self}.chat(
|
|
436
451
|
request: 'required - message to Gemini',
|
|
437
452
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:gemini][:model])',
|
data/lib/pwn/ai/grok.rb
CHANGED
|
@@ -382,7 +382,7 @@ module PWN
|
|
|
382
382
|
real_config_value?(value: oauth[:bearer_token]) ||
|
|
383
383
|
real_config_value?(value: oauth[:refresh_token])
|
|
384
384
|
|
|
385
|
-
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key]))
|
|
385
|
+
if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key])) && !opts[:non_interactive]
|
|
386
386
|
# Singular device-flow enrollment. Result is written back into the
|
|
387
387
|
# live oauth hash so subsequent grok_rest_call invocations inside the
|
|
388
388
|
# same pwn / pwn-ai process reuse it silently.
|
|
@@ -391,9 +391,15 @@ module PWN
|
|
|
391
391
|
|
|
392
392
|
token = engine[:key] if token.nil? && real_config_value?(value: engine[:key])
|
|
393
393
|
|
|
394
|
-
token
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
if token.nil?
|
|
395
|
+
return nil if opts[:non_interactive]
|
|
396
|
+
|
|
397
|
+
token = PWN::Plugins::AuthenticationHelper.mask_password(
|
|
398
|
+
prompt: 'Grok API Key (or run PWN::AI::Grok.obtain_oauth_bearer_token for SuperGrok OAuth)'
|
|
399
|
+
)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
return nil if token.nil?
|
|
397
403
|
|
|
398
404
|
http_method = opts[:http_method].nil? ? :get : opts[:http_method].to_s.scrub.to_sym
|
|
399
405
|
|
|
@@ -468,15 +474,24 @@ module PWN
|
|
|
468
474
|
|
|
469
475
|
retry
|
|
470
476
|
end
|
|
477
|
+
rescue RestClient::Exceptions::Timeout => e
|
|
478
|
+
# Sidecar hops (judge / kind / plan) pass quiet:true and a short
|
|
479
|
+
# timeout. Do not print ERROR: Timed out reading data from server:
|
|
480
|
+
# — ReadTimeout is ExceptionWithResponse with a nil body.
|
|
481
|
+
warn "[pwn-ai/grok] #{e.class}: #{e.message} (#{http_method.to_s.upcase} #{rest_call} timeout=#{timeout}s)" unless opts[:quiet]
|
|
482
|
+
nil
|
|
471
483
|
rescue RestClient::ExceptionWithResponse => e
|
|
472
|
-
puts "ERROR: #{e.message}: #{e.response}"
|
|
484
|
+
puts "ERROR: #{e.message}: #{e.response}" unless opts[:quiet]
|
|
485
|
+
"#{e.message}: #{e.response}" if opts[:quiet]
|
|
473
486
|
rescue StandardError => e
|
|
474
487
|
case e.message
|
|
475
488
|
when '400 Bad Request', '404 Resource Not Found'
|
|
476
|
-
|
|
489
|
+
nil
|
|
477
490
|
else
|
|
478
|
-
raise e
|
|
491
|
+
raise e unless opts[:quiet]
|
|
492
|
+
|
|
479
493
|
end
|
|
494
|
+
"#{e.message}: #{e.response}"
|
|
480
495
|
ensure
|
|
481
496
|
spin.stop if spinner
|
|
482
497
|
end
|
|
@@ -492,6 +507,162 @@ module PWN
|
|
|
492
507
|
raise e
|
|
493
508
|
end
|
|
494
509
|
|
|
510
|
+
# Supported Method Parameters::
|
|
511
|
+
# usage = PWN::AI::Grok.get_plan_usage(
|
|
512
|
+
# timeout: 'optional - seconds (default 8)'
|
|
513
|
+
# )
|
|
514
|
+
#
|
|
515
|
+
# Subscription / team usage for the PS1 percent suffix.
|
|
516
|
+
# GET /v1/api-key yields team_id; then billing preview /
|
|
517
|
+
# spending-limits / prepaid balance compute used vs limit.
|
|
518
|
+
# GET /v1/me is also tried (OAuth personal-team payload).
|
|
519
|
+
public_class_method def self.get_plan_usage(opts = {})
|
|
520
|
+
timeout = opts[:timeout] || 8
|
|
521
|
+
return { available: false, engine: :grok } unless plan_usage_credentials?
|
|
522
|
+
|
|
523
|
+
me = parse_plan_usage_json(
|
|
524
|
+
raw: grok_rest_call(rest_call: 'me', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
|
|
525
|
+
)
|
|
526
|
+
if me.is_a?(Hash)
|
|
527
|
+
used = first_numeric(src: me, keys: %i[used usage spend spent used_usd usage_usd])
|
|
528
|
+
limit = first_numeric(src: me, keys: %i[limit quota allowance hard_limit spending_limit])
|
|
529
|
+
normalized = PWN::AI.normalize_plan_usage(
|
|
530
|
+
used: used,
|
|
531
|
+
limit: limit,
|
|
532
|
+
source: 'me',
|
|
533
|
+
engine: :grok
|
|
534
|
+
)
|
|
535
|
+
return normalized if normalized[:available]
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
key_info = parse_plan_usage_json(
|
|
539
|
+
raw: grok_rest_call(rest_call: 'api-key', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
|
|
540
|
+
)
|
|
541
|
+
team_id = key_info.is_a?(Hash) ? key_info[:team_id] : nil
|
|
542
|
+
team_id ||= me.is_a?(Hash) ? (me[:team_id] || me.dig(:team, :id)) : nil
|
|
543
|
+
|
|
544
|
+
if team_id.to_s.strip != ''
|
|
545
|
+
preview = parse_plan_usage_json(
|
|
546
|
+
raw: grok_rest_call(
|
|
547
|
+
rest_call: "billing/teams/#{team_id}/postpaid/invoice/preview",
|
|
548
|
+
timeout: timeout,
|
|
549
|
+
spinner: false,
|
|
550
|
+
quiet: true,
|
|
551
|
+
non_interactive: true
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
limits = parse_plan_usage_json(
|
|
555
|
+
raw: grok_rest_call(
|
|
556
|
+
rest_call: "billing/teams/#{team_id}/postpaid/spending-limits",
|
|
557
|
+
timeout: timeout,
|
|
558
|
+
spinner: false,
|
|
559
|
+
quiet: true,
|
|
560
|
+
non_interactive: true
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
balance = parse_plan_usage_json(
|
|
564
|
+
raw: grok_rest_call(
|
|
565
|
+
rest_call: "billing/teams/#{team_id}/prepaid/balance",
|
|
566
|
+
timeout: timeout,
|
|
567
|
+
spinner: false,
|
|
568
|
+
quiet: true,
|
|
569
|
+
non_interactive: true
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
used = nil
|
|
574
|
+
limit = nil
|
|
575
|
+
if preview.is_a?(Hash)
|
|
576
|
+
core = preview[:coreInvoice] || preview[:core_invoice] || {}
|
|
577
|
+
used_cents = cents_val(src: core, key: :prepaidCreditsUsed) ||
|
|
578
|
+
cents_val(src: core, key: :prepaid_credits_used) ||
|
|
579
|
+
core[:amountAfterVat] ||
|
|
580
|
+
core[:amount_after_vat]
|
|
581
|
+
prepaid_cents = cents_val(src: core, key: :prepaidCredits) ||
|
|
582
|
+
cents_val(src: core, key: :prepaid_credits)
|
|
583
|
+
spend_limit_cents = preview[:effectiveSpendingLimit] || preview[:effective_spending_limit]
|
|
584
|
+
used = to_f_or_nil(value: used_cents)
|
|
585
|
+
limit = to_f_or_nil(value: spend_limit_cents)
|
|
586
|
+
limit = to_f_or_nil(value: prepaid_cents).abs if (limit.nil? || limit <= 0) && prepaid_cents
|
|
587
|
+
end
|
|
588
|
+
used = to_f_or_nil(value: cents_val(src: balance, key: :total)).to_f.abs if (used.nil? || used <= 0) && balance.is_a?(Hash)
|
|
589
|
+
if (limit.nil? || limit <= 0) && limits.is_a?(Hash)
|
|
590
|
+
sl = limits[:spendingLimits] || limits[:spending_limits] || {}
|
|
591
|
+
limit = to_f_or_nil(value: cents_val(src: sl, key: :effectiveSl)) ||
|
|
592
|
+
to_f_or_nil(value: cents_val(src: sl, key: :effectiveHardSl)) ||
|
|
593
|
+
to_f_or_nil(value: cents_val(src: sl, key: :softSl))
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
normalized = PWN::AI.normalize_plan_usage(
|
|
597
|
+
used: used,
|
|
598
|
+
limit: limit,
|
|
599
|
+
source: 'billing/teams',
|
|
600
|
+
engine: :grok
|
|
601
|
+
)
|
|
602
|
+
return normalized if normalized[:available]
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
{ available: false, engine: :grok }
|
|
606
|
+
rescue StandardError
|
|
607
|
+
{ available: false, engine: :grok }
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
private_class_method def self.plan_usage_credentials?
|
|
611
|
+
engine = PWN::Env.dig(:ai, :grok) || {}
|
|
612
|
+
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
613
|
+
real_config_value?(value: engine[:key]) ||
|
|
614
|
+
real_config_value?(value: oauth[:bearer_token]) ||
|
|
615
|
+
real_config_value?(value: oauth[:refresh_token])
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
619
|
+
raw = opts[:raw]
|
|
620
|
+
return nil if raw.nil?
|
|
621
|
+
|
|
622
|
+
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
623
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
624
|
+
rescue JSON::ParserError, TypeError
|
|
625
|
+
nil
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
private_class_method def self.cents_val(opts = {})
|
|
629
|
+
src = opts[:src]
|
|
630
|
+
key = opts[:key]
|
|
631
|
+
return nil unless src.is_a?(Hash)
|
|
632
|
+
|
|
633
|
+
v = src[key]
|
|
634
|
+
return v[:val] if v.is_a?(Hash) && v.key?(:val)
|
|
635
|
+
return v['val'] if v.is_a?(Hash) && v.key?('val')
|
|
636
|
+
|
|
637
|
+
v
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
private_class_method def self.to_f_or_nil(opts = {})
|
|
641
|
+
val = opts.is_a?(Hash) && opts.key?(:value) ? opts[:value] : opts
|
|
642
|
+
return nil if val.nil?
|
|
643
|
+
return val.to_f if val.respond_to?(:to_f)
|
|
644
|
+
|
|
645
|
+
nil
|
|
646
|
+
rescue StandardError
|
|
647
|
+
nil
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
private_class_method def self.first_numeric(opts = {})
|
|
651
|
+
src = opts[:src]
|
|
652
|
+
keys = Array(opts[:keys])
|
|
653
|
+
return nil unless src.is_a?(Hash)
|
|
654
|
+
|
|
655
|
+
keys.each do |k|
|
|
656
|
+
v = src[k]
|
|
657
|
+
next if v.nil?
|
|
658
|
+
next if v.is_a?(Hash)
|
|
659
|
+
|
|
660
|
+
f = v.to_f
|
|
661
|
+
return f if f.positive? || v.to_s.strip.match?(/\A-?\d/)
|
|
662
|
+
end
|
|
663
|
+
nil
|
|
664
|
+
end
|
|
665
|
+
|
|
495
666
|
# Supported Method Parameters::
|
|
496
667
|
# response = PWN::AI::Grok.chat_with_tools(
|
|
497
668
|
# messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -536,7 +707,8 @@ module PWN
|
|
|
536
707
|
rest_call: 'chat/completions',
|
|
537
708
|
http_body: http_body,
|
|
538
709
|
timeout: opts[:timeout],
|
|
539
|
-
spinner: opts[:spinner]
|
|
710
|
+
spinner: opts[:spinner],
|
|
711
|
+
quiet: opts[:quiet]
|
|
540
712
|
)
|
|
541
713
|
return nil if response.nil?
|
|
542
714
|
|
|
@@ -616,8 +788,10 @@ module PWN
|
|
|
616
788
|
rest_call: rest_call,
|
|
617
789
|
http_body: http_body,
|
|
618
790
|
timeout: timeout,
|
|
619
|
-
spinner: spinner
|
|
791
|
+
spinner: spinner,
|
|
792
|
+
quiet: opts[:quiet]
|
|
620
793
|
)
|
|
794
|
+
return nil if response.nil? || response.to_s.strip.empty?
|
|
621
795
|
|
|
622
796
|
json_resp = JSON.parse(response, symbolize_names: true)
|
|
623
797
|
assistant_resp = json_resp[:choices].first[:message]
|
|
@@ -655,6 +829,8 @@ module PWN
|
|
|
655
829
|
puts "USAGE:
|
|
656
830
|
models = #{self}.get_models
|
|
657
831
|
|
|
832
|
+
usage = #{self}.get_plan_usage
|
|
833
|
+
|
|
658
834
|
response = #{self}.chat(
|
|
659
835
|
request: 'required - message to Grok',
|
|
660
836
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:grok][:model])',
|