portage-cli 0.7.5 → 0.9.0
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/CHANGELOG.md +616 -0
- data/README.md +302 -5
- data/known-stores/categories.yml +1263 -0
- data/lib/portage/cli/agent_profile_url.rb +30 -0
- data/lib/portage/cli/approval_policy.rb +56 -0
- data/lib/portage/cli/approve.rb +135 -0
- data/lib/portage/cli/browser_import/categorize.rb +59 -0
- data/lib/portage/cli/browser_import/confirm.rb +35 -0
- data/lib/portage/cli/browser_import/domains.rb +47 -0
- data/lib/portage/cli/browser_import/filter.rb +91 -0
- data/lib/portage/cli/browser_import/importer.rb +248 -0
- data/lib/portage/cli/browser_import/plist_xml.rb +72 -0
- data/lib/portage/cli/browser_import/prober.rb +60 -0
- data/lib/portage/cli/browser_import/profiles.rb +114 -0
- data/lib/portage/cli/browser_import/readers.rb +179 -0
- data/lib/portage/cli/browser_import/saver.rb +62 -0
- data/lib/portage/cli/browser_import/sqlite.rb +68 -0
- data/lib/portage/cli/browser_import.rb +23 -0
- data/lib/portage/cli/browser_opener.rb +38 -0
- data/lib/portage/cli/browser_profile/allowlist.rb +40 -0
- data/lib/portage/cli/browser_profile/bridge.rb +120 -0
- data/lib/portage/cli/browser_profile/browsers.rb +69 -0
- data/lib/portage/cli/browser_profile/cdp.rb +67 -0
- data/lib/portage/cli/browser_profile/cdp_socket.rb +186 -0
- data/lib/portage/cli/browser_profile/errors.rb +26 -0
- data/lib/portage/cli/browser_profile/launcher.rb +34 -0
- data/lib/portage/cli/browser_profile/profile.rb +93 -0
- data/lib/portage/cli/browser_profile.rb +25 -0
- data/lib/portage/cli/buy.rb +559 -29
- data/lib/portage/cli/checkout_handoff.rb +5 -24
- data/lib/portage/cli/classifier.rb +158 -0
- data/lib/portage/cli/compare.rb +3 -0
- data/lib/portage/cli/doctor.rb +155 -1
- data/lib/portage/cli/dot_env.rb +55 -0
- data/lib/portage/cli/find.rb +103 -12
- data/lib/portage/cli/handoff_agents.rb +186 -0
- data/lib/portage/cli/handoff_only.rb +94 -0
- data/lib/portage/cli/handoff_reconciler.rb +15 -1
- data/lib/portage/cli/handoff_target.rb +61 -0
- data/lib/portage/cli/history.rb +47 -2
- data/lib/portage/cli/human_prompt.rb +118 -0
- data/lib/portage/cli/index/builder.rb +335 -0
- data/lib/portage/cli/index/exporter.rb +91 -0
- data/lib/portage/cli/index/known_cache.rb +155 -0
- data/lib/portage/cli/index/product_store.rb +101 -0
- data/lib/portage/cli/index/sources/browser.rb +31 -0
- data/lib/portage/cli/index/sources/shopify_catalog.rb +82 -0
- data/lib/portage/cli/index/sources/stores_file.rb +58 -0
- data/lib/portage/cli/index/sources/webmcp_sweep.rb +29 -0
- data/lib/portage/cli/index/sources/wikidata.rb +95 -0
- data/lib/portage/cli/index/sources.rb +44 -0
- data/lib/portage/cli/index/store.rb +109 -0
- data/lib/portage/cli/index.rb +20 -0
- data/lib/portage/cli/known_stores_url.rb +15 -0
- data/lib/portage/cli/money.rb +18 -0
- data/lib/portage/cli/offer_choice.rb +38 -0
- data/lib/portage/cli/offer_sources.rb +460 -0
- data/lib/portage/cli/payment_methods.rb +24 -3
- data/lib/portage/cli/pick.rb +167 -0
- data/lib/portage/cli/product_page.rb +84 -0
- data/lib/portage/cli/quotes.rb +82 -0
- data/lib/portage/cli/search_backends.rb +337 -12
- data/lib/portage/cli/setup_wizard/prompt.rb +67 -0
- data/lib/portage/cli/setup_wizard/steps/agent_profile.rb +60 -0
- data/lib/portage/cli/setup_wizard/steps/browser_import.rb +29 -0
- data/lib/portage/cli/setup_wizard/steps/handoff.rb +100 -0
- data/lib/portage/cli/setup_wizard/steps/index_build.rb +31 -0
- data/lib/portage/cli/setup_wizard/steps/policy.rb +60 -0
- data/lib/portage/cli/setup_wizard/steps/retailer_keys.rb +55 -0
- data/lib/portage/cli/setup_wizard/steps/search_keys.rb +54 -0
- data/lib/portage/cli/setup_wizard/steps/shipping.rb +51 -0
- data/lib/portage/cli/setup_wizard.rb +74 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli/webmcp.rb +10 -3
- data/lib/portage/cli/webmcp_autofill_confirm.rb +38 -0
- data/lib/portage/cli/webmcp_autofill_fields.rb +60 -0
- data/lib/portage/cli/webmcp_autofill_mode.rb +39 -0
- data/lib/portage/cli/webmcp_mapping_confirm.rb +68 -0
- data/lib/portage/cli/webmcp_mappings.rb +84 -0
- data/lib/portage/cli.rb +930 -43
- metadata +67 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require_relative "config"
|
|
2
|
+
require_relative "setting"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Cli
|
|
6
|
+
# docs/plans/webmcp-universal-outbound.md Phase 3 — the opt-in gate in
|
|
7
|
+
# front of filling the store's own checkout page. Off by default: only
|
|
8
|
+
# `--autofill` (a per-run CLI flag, forwarded to `Buy.new(autofill:)`)
|
|
9
|
+
# or `PORTAGE_WEBMCP_AUTOFILL=approve` / config.json's
|
|
10
|
+
# `"webmcp_autofill": "approve"` turn it on. Same Setting precedence
|
|
11
|
+
# everywhere else in this gem uses (WebmcpCheckoutMode, HandoffSpendMode)
|
|
12
|
+
# — override beats env beats config — except the override here is a
|
|
13
|
+
# boolean (the flag is either passed or it isn't), while the env/config
|
|
14
|
+
# level is a literal string, `"approve"`, not truthy-string parsing
|
|
15
|
+
# (Setting.flag?'s "1"/"true"/"yes"): a plan that could be
|
|
16
|
+
# `PORTAGE_WEBMCP_AUTOFILL=true` by accident from some other truthy
|
|
17
|
+
# convention shouldn't silently turn on typing into a checkout page.
|
|
18
|
+
#
|
|
19
|
+
# Turning this gate on is still only half of Phase 3's opt-in: even
|
|
20
|
+
# approved, nothing is typed until the shopper also approves the exact
|
|
21
|
+
# field/value pairs in WebmcpAutofillConfirm's own prompt.
|
|
22
|
+
module WebmcpAutofillMode
|
|
23
|
+
ENV_VAR = "PORTAGE_WEBMCP_AUTOFILL".freeze
|
|
24
|
+
CONFIG_KEY = "webmcp_autofill".freeze
|
|
25
|
+
APPROVE = "approve".freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# @param override [Boolean, nil] --autofill's value: true when the
|
|
30
|
+
# flag was passed, nil otherwise (there's no --no-autofill; the flag
|
|
31
|
+
# only ever turns this on, same as --dry-run/--yes elsewhere in Buy).
|
|
32
|
+
def approved?(override: nil, config: Config.load)
|
|
33
|
+
return true if override == true
|
|
34
|
+
|
|
35
|
+
Setting.resolve(env: ENV_VAR, config: config, config_key: CONFIG_KEY).to_s.strip.downcase == APPROVE
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Cli
|
|
3
|
+
# Turns a `Portage::Ucp::WebMcp::Matcher` proposal into `tool_names:`
|
|
4
|
+
# `Buy#webmcp_flow` can pass to `WebMcp.connect`, enforcing Phase 2's
|
|
5
|
+
# confirm-before-mutate rule (docs/plans/webmcp-universal-outbound.md):
|
|
6
|
+
# a read action (`search_catalog`, `get_product`, `get_cart`) is usable
|
|
7
|
+
# straight off the proposal — wrong at worst wastes a call. A mutating
|
|
8
|
+
# action (`create_cart`, `update_cart`, `create_checkout`) needs
|
|
9
|
+
# confirmation first, since a wrong guess there could add to, discard,
|
|
10
|
+
# or charge a stranger's cart.
|
|
11
|
+
#
|
|
12
|
+
# The prompt quotes a mutating tool's own `description` as what it is —
|
|
13
|
+
# page content, therefore untrusted — never as an instruction. Nothing
|
|
14
|
+
# here executes, follows, or acts on anything the description says; it
|
|
15
|
+
# is printed for the shopper to read and nothing more.
|
|
16
|
+
class WebmcpMappingConfirm
|
|
17
|
+
READ_ACTIONS = %w[search_catalog get_product get_cart].freeze
|
|
18
|
+
|
|
19
|
+
# @param interactive [Boolean] whether a prompt can actually be shown
|
|
20
|
+
# and answered — false under --json or with no TTY on stdin (the
|
|
21
|
+
# caller decides; this class has no opinion on how). false means a
|
|
22
|
+
# proposal with any mutating action is always refused.
|
|
23
|
+
def initialize(interactive:, input: $stdin, output: $stdout)
|
|
24
|
+
@interactive = interactive
|
|
25
|
+
@input = input
|
|
26
|
+
@output = output
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param proposal [Hash{String => Matcher::Proposal}]
|
|
30
|
+
# @param tools [Array<Hash>] the page's own tools, so the prompt can
|
|
31
|
+
# quote a mutating tool's description.
|
|
32
|
+
# @return [Hash{String => String}, nil] a tool_names: hash covering
|
|
33
|
+
# every proposed action, or nil when a mutating action was proposed
|
|
34
|
+
# and couldn't be confirmed — no TTY, --json, or the shopper
|
|
35
|
+
# declined. Never returns a partial mapping: either every mutating
|
|
36
|
+
# action in the proposal is approved, or none of them are used.
|
|
37
|
+
def call(proposal, tools)
|
|
38
|
+
reads = proposal.slice(*READ_ACTIONS).transform_values(&:tool)
|
|
39
|
+
mutating = proposal.except(*READ_ACTIONS)
|
|
40
|
+
return reads if mutating.empty?
|
|
41
|
+
return nil unless confirmed?(mutating, tools)
|
|
42
|
+
|
|
43
|
+
reads.merge(mutating.transform_values(&:tool))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def confirmed?(mutating, tools) = @interactive && approve?(mutating, tools)
|
|
49
|
+
|
|
50
|
+
def approve?(mutating, tools)
|
|
51
|
+
@output.puts "This page's WebMCP tools weren't recognized as a known store platform. Based on their " \
|
|
52
|
+
"names and schemas, here's the proposed mapping for cart/checkout actions — the " \
|
|
53
|
+
"description text is quoted from the page itself, not a trusted instruction:"
|
|
54
|
+
mutating.each { |action, match| describe(action, match, tools) }
|
|
55
|
+
@output.print "Use this mapping? [y/N] "
|
|
56
|
+
@output.flush
|
|
57
|
+
@input.gets.to_s.strip.downcase == "y"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def describe(action, match, tools)
|
|
61
|
+
tool = tools.find { |t| (t["name"] || t[:name]).to_s == match.tool }
|
|
62
|
+
description = tool && (tool["description"] || tool[:description])
|
|
63
|
+
@output.puts " #{action} -> #{match.tool} (confidence #{match.confidence}) — #{match.reason}"
|
|
64
|
+
@output.puts " page description: #{description.inspect}" if description
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Portage
|
|
6
|
+
module Cli
|
|
7
|
+
# Confirmed Phase 2 `tool_names:` mappings — `~/.portage/webmcp_mappings.json`
|
|
8
|
+
# (docs/plans/webmcp-universal-outbound.md) — keyed by the page's tool
|
|
9
|
+
# fingerprint (`Portage::Ucp::WebMcp::Fingerprint.for`: sorted tool names
|
|
10
|
+
# plus a hash of each one's own input schema), never by origin
|
|
11
|
+
# (decision 3): once a shopper confirms a mapping for one store, any
|
|
12
|
+
# other store whose WebMCP tools have the exact same shape reuses it
|
|
13
|
+
# with no prompt at all — in effect a local preset. A lookalike page
|
|
14
|
+
# whose schemas differ even slightly gets a different fingerprint and
|
|
15
|
+
# has to be confirmed again, so it can't borrow a mapping with a
|
|
16
|
+
# different shape underneath the same tool names.
|
|
17
|
+
#
|
|
18
|
+
# Same "absent file or absent key means unset, a corrupt file raises
|
|
19
|
+
# rather than silently falling back" posture as Config — this is
|
|
20
|
+
# deliberately its own file, not a key under config.json, since it's
|
|
21
|
+
# confirmed data (grows with use) rather than a standing preference.
|
|
22
|
+
class WebmcpMappings
|
|
23
|
+
PATH = File.join(Dir.home, ".portage", "webmcp_mappings.json").freeze
|
|
24
|
+
|
|
25
|
+
def self.load(path: PATH) = new(path: path, data: read(path))
|
|
26
|
+
|
|
27
|
+
def initialize(path: PATH, data: {})
|
|
28
|
+
@path = path
|
|
29
|
+
@data = data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param tools [Array<Hash>] the page's tools, as list_tools returns
|
|
33
|
+
# them.
|
|
34
|
+
# @return [Hash{String => String}, nil] the confirmed tool_names: for
|
|
35
|
+
# this exact fingerprint, or nil when none has been confirmed yet.
|
|
36
|
+
def lookup(tools)
|
|
37
|
+
entry = @data[fingerprint_for(tools)]
|
|
38
|
+
entry && entry["tool_names"].transform_keys(&:to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param tool_names [Hash] action => tool name, as just confirmed.
|
|
42
|
+
# Merged onto whatever this fingerprint already had (so confirming a
|
|
43
|
+
# new action later doesn't drop an earlier one), never replaced
|
|
44
|
+
# outright.
|
|
45
|
+
# @param origin [String, nil] recorded as metadata only — the lookup
|
|
46
|
+
# key is the fingerprint alone (decision 3), never the origin.
|
|
47
|
+
# @return [Hash{String => String}] the fingerprint's tool_names: after
|
|
48
|
+
# merging.
|
|
49
|
+
def confirm!(tools, tool_names:, origin: nil)
|
|
50
|
+
key = fingerprint_for(tools)
|
|
51
|
+
entry = @data[key] || { "tool_names" => {}, "origins" => [] }
|
|
52
|
+
entry["tool_names"] = entry["tool_names"].merge(tool_names.transform_keys(&:to_s))
|
|
53
|
+
entry["origins"] = (entry["origins"] + [origin]).compact.uniq if origin
|
|
54
|
+
entry["confirmed_at"] = Time.now.utc.iso8601
|
|
55
|
+
@data[key] = entry
|
|
56
|
+
write
|
|
57
|
+
entry["tool_names"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h = @data.dup
|
|
61
|
+
|
|
62
|
+
def self.read(path)
|
|
63
|
+
return {} unless File.readable?(path)
|
|
64
|
+
|
|
65
|
+
raw = File.read(path)
|
|
66
|
+
return {} if raw.empty?
|
|
67
|
+
|
|
68
|
+
parsed = JSON.parse(raw)
|
|
69
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
70
|
+
end
|
|
71
|
+
private_class_method :read
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def fingerprint_for(tools) = Portage::Ucp::WebMcp::Fingerprint.for(tools)
|
|
76
|
+
|
|
77
|
+
def write
|
|
78
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
79
|
+
File.write(@path, JSON.pretty_generate(@data))
|
|
80
|
+
File.chmod(0o600, @path)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|