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
data/README.md
CHANGED
|
@@ -123,13 +123,19 @@ use the gem's. Or reorder `PATH` so the one you want comes first.
|
|
|
123
123
|
```bash
|
|
124
124
|
portage buy <url> --query "..." [--qty N] [--payment-token TOKEN] [--product-id ID]
|
|
125
125
|
[--yes] [--dry-run] [--auto-open|--no-auto-open]
|
|
126
|
-
[--notify-webhook URL]
|
|
127
|
-
[--
|
|
126
|
+
[--notify-webhook URL]
|
|
127
|
+
[--handoff-target default|print|profile|agent:NAME]
|
|
128
|
+
[--decision-backend jev|laya] [--min-confidence N] [--json]
|
|
128
129
|
[--wait [--wait-timeout DURATION|off]]
|
|
130
|
+
portage buy --offer REF [--qty N] [--yes] [--dry-run] ...
|
|
131
|
+
portage buy --quote QUOTE_ID --yes [--json] ...
|
|
129
132
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
130
133
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
131
134
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
132
135
|
[--max-price N] [--json]
|
|
136
|
+
portage pick [--search LAST|SEARCH_ID] [--via auto|tty|agent] [--json]
|
|
137
|
+
[--choose REF | --compare REF | --view REF]
|
|
138
|
+
portage approve QUOTE_ID [--via auto|tty|agent] [--relayed-yes | --view] [--json]
|
|
133
139
|
portage history [list] [--purchases|--searches] [--limit N] [--json]
|
|
134
140
|
portage history clear [--purchases|--searches]
|
|
135
141
|
portage payment list [--json]
|
|
@@ -144,8 +150,21 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
144
150
|
[--rolling-cap N --rolling-window-seconds N --currency CUR]
|
|
145
151
|
[--velocity-count N --velocity-window-seconds N]
|
|
146
152
|
[--allow HOST ...] [--clear-allowlist]
|
|
153
|
+
[--require-approval person|any|off] (lowering asks at a terminal)
|
|
147
154
|
portage orders reconcile [--checkout ID] [--json]
|
|
148
|
-
portage
|
|
155
|
+
portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
156
|
+
portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
157
|
+
portage index show [--stores|--products] [--json]
|
|
158
|
+
portage index add <url> [--json]
|
|
159
|
+
portage index remove <host> [--json]
|
|
160
|
+
portage index sources [--json]
|
|
161
|
+
portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
|
|
162
|
+
[--history-days 90] [--include-product-pages] [--max-probes 200]
|
|
163
|
+
[--exclude HOST,HOST] [--dry-run] [--yes] [--json]
|
|
164
|
+
portage browser profile init|open|status [--browser chrome|edge|brave|arc] [--port N]
|
|
165
|
+
[--url URL (open only)] [--json]
|
|
166
|
+
portage doctor [--require FILE] [--adapter CLASS_NAME] [--json] # alias: configure
|
|
167
|
+
portage setup [--json] # interactive wizard on a TTY; --json/no TTY: today's doctor report
|
|
149
168
|
portage generate adapter NAME [--dir DIR]
|
|
150
169
|
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
151
170
|
portage --version
|
|
@@ -188,6 +207,8 @@ See "Proxy" below.
|
|
|
188
207
|
`PORTAGE_MIN_CONFIDENCE`, then `0.8`. The flag is always checked; the env
|
|
189
208
|
var is read (and checked) only while a backend is selected, so a stale
|
|
190
209
|
value can't block a buy that doesn't use the gate.
|
|
210
|
+
- `--handoff-target default|print|profile|agent:NAME` — where a dead-end
|
|
211
|
+
checkout's link goes. See "Hand-off targets and hand-off-only hosts" below.
|
|
191
212
|
- `--json` — machine-readable report instead of the human-readable summary.
|
|
192
213
|
|
|
193
214
|
Exits `0` when a checkout completed (or a dry-run/browse/search resolved
|
|
@@ -346,6 +367,222 @@ opt-in guardrail, not a default-deny one. Per-token scopes (merchant/amount
|
|
|
346
367
|
limits bound to one enrolled card) are set via `portage payment enroll
|
|
347
368
|
--scope-*` above, not here.
|
|
348
369
|
|
|
370
|
+
`portage policy set --require-approval person|any|off` (default `any`) sets what a
|
|
371
|
+
real `buy --yes` needs: `off` is `--yes` alone; `any` needs `--quote QUOTE_ID` for a
|
|
372
|
+
quote approved with `portage approve` (by the person, or relayed by an agent with
|
|
373
|
+
`--relayed-yes`); `person` needs the person's own yes at a terminal. Otherwise the run
|
|
374
|
+
is a dry run that returns `needs_approval`. Lowering the level asks for a yes at a
|
|
375
|
+
terminal. Stored as `require_approval` in `~/.portage/policy.json`. It raises the bar
|
|
376
|
+
against an agent but isn't a hard guarantee: a process with a shell can edit that file
|
|
377
|
+
or the quote files in `~/.portage/quotes/`. The whole flow (`find`, `pick`, `buy
|
|
378
|
+
--offer --dry-run`, `approve`, `buy --quote --yes`) is in the
|
|
379
|
+
[CLI JSON reference](../docs/api/cli-json.md) and the
|
|
380
|
+
[tutorial](../docs/cli-usage-tutorial.md#picking-and-approving-at-the-terminal). Upgrade
|
|
381
|
+
note: under the default `any`, `buy --yes` with no approved `--quote` no longer buys;
|
|
382
|
+
restore the old behaviour with `portage policy set --require-approval off` from a
|
|
383
|
+
terminal.
|
|
384
|
+
|
|
385
|
+
### Tiers: how a purchase actually finishes
|
|
386
|
+
|
|
387
|
+
Most stores don't let a third-party agent complete payment. `portage buy`
|
|
388
|
+
never pretends otherwise — it builds the cart/checkout it can, then hands
|
|
389
|
+
off through one of three tiers, from least to most involved:
|
|
390
|
+
|
|
391
|
+
| Tier | What | Default | Guardrail |
|
|
392
|
+
| --- | --- | --- | --- |
|
|
393
|
+
| A | Hand off to your own default browser; optionally seed the store index from your bookmarks/history (`portage browser import`) | Hand-off on; import opt-in | Domains only; you see and approve the imported list; nothing leaves the machine |
|
|
394
|
+
| B | A dedicated Portage browser profile drives the cart via WebMCP, then hands off there for you to pay (`portage browser profile`) | Off | Never your default profile; a domain allowlist (the store plus its checkout host); stops at payment — you click pay, your browser's own card autofill fills it |
|
|
395
|
+
| C | Hand-off-only hosts (Amazon, and any host you add) — Portage opens the page or a search/cart-add URL and you buy | On by default, host list is yours to edit | No scraping, no page reads, no UCP probe — just a URL, built not fetched |
|
|
396
|
+
|
|
397
|
+
**Never, in any tier:** Portage reading your browser's password, cookie or
|
|
398
|
+
autofill store; Portage attaching to your default browser profile; Portage
|
|
399
|
+
solving or bypassing a CAPTCHA; card data passing through Portage.
|
|
400
|
+
|
|
401
|
+
### Hand-off targets and hand-off-only hosts
|
|
402
|
+
|
|
403
|
+
Most checkouts end in a hand-off, not a `purchased` outcome — see the next
|
|
404
|
+
section. `--handoff-target default|print|profile|agent:<name>`
|
|
405
|
+
(`PORTAGE_HANDOFF_TARGET`, or `~/.portage/config.json`'s `"handoff_target"`)
|
|
406
|
+
decides where that link goes:
|
|
407
|
+
|
|
408
|
+
- `default` (Tier A) — opens it in your own browser. Today's behaviour:
|
|
409
|
+
`--auto-open`/`--no-auto-open`, `PORTAGE_AUTO_OPEN_CHECKOUT`.
|
|
410
|
+
- `print` — just reports the URL.
|
|
411
|
+
- `profile` (Tier B) — drives the dedicated Portage browser profile (see
|
|
412
|
+
"Portage browser profile" below) instead of your own. With no profile
|
|
413
|
+
attached (not opened yet, or `portage-ucp-webmcp` isn't installed), it
|
|
414
|
+
reports that and falls back to reporting the link.
|
|
415
|
+
- `agent:<name>` — hands the checkout URL and cart summary (items, qty,
|
|
416
|
+
total, store — the same JSON `--notify-webhook` sends) to an external
|
|
417
|
+
agent you've approved once in `~/.portage/config.json`'s
|
|
418
|
+
`"handoff_agents"` (a command, run with a scrubbed environment and the
|
|
419
|
+
payload on stdin, or an `https` webhook — never invoked unless
|
|
420
|
+
`"approved": true`, and never given credentials, payment tokens or
|
|
421
|
+
shipping details beyond what the checkout URL already holds).
|
|
422
|
+
|
|
423
|
+
An unrecognized value is a usage error (`invalid_option` under `--json`),
|
|
424
|
+
checked before the buy starts.
|
|
425
|
+
|
|
426
|
+
Amazon (every marketplace TLD), walmart.com, ebay.com and bestbuy.com
|
|
427
|
+
(unconditionally — no adapter, no UCP for any of them to opt back into), and
|
|
428
|
+
any host in `~/.portage/config.json`'s `"handoff_only_hosts"` are **Tier C,
|
|
429
|
+
hand-off only**: `portage buy` never sends that host a request at all — no
|
|
430
|
+
UCP probe, no page fetch, no cart — it opens the page (or a cart-add/search
|
|
431
|
+
URL when a product id is known) and you buy it yourself. Absent that config
|
|
432
|
+
key, the default list is every Amazon marketplace; once present, your list
|
|
433
|
+
*is* the list — drop Amazon or add another host, and removing one only
|
|
434
|
+
changes the message, since there's no code here that automates a site
|
|
435
|
+
without UCP or WebMCP. `find`, `index build` and `browser import` all skip
|
|
436
|
+
probing a hand-off-only host too, though they may still list it as a
|
|
437
|
+
candidate you already know about. `portage doctor` reports the current
|
|
438
|
+
target and host list. **Portage is open-source software provided as-is,
|
|
439
|
+
without warranty of any kind (MIT)** — how it's used on any site, and
|
|
440
|
+
compliance with that site's terms, is your own responsibility.
|
|
441
|
+
|
|
442
|
+
### Categories and routing
|
|
443
|
+
|
|
444
|
+
`portage find` classifies your query and a store's title/description/URL
|
|
445
|
+
slug against `known-stores/categories.yml` (the top two levels of Google's
|
|
446
|
+
published product taxonomy, ~200 nodes shipped in the gem;
|
|
447
|
+
`~/.portage/categories.yml` overrides or extends it). A tagged
|
|
448
|
+
`stores.yml`/index entry (`{url:, categories: [...]}`) only spends one of a
|
|
449
|
+
query's probe slots when its categories actually match — capped at 3 stores
|
|
450
|
+
per category and 12 total — so a large personal allowlist or a big local
|
|
451
|
+
index doesn't crowd out the store that actually sells what you asked for. An
|
|
452
|
+
entry with no matching category is still reached when you name it by host or
|
|
453
|
+
brand.
|
|
454
|
+
|
|
455
|
+
### Local store index (`portage index`)
|
|
456
|
+
|
|
457
|
+
A fresh install only knows the stores you type into `stores.yml` or that a
|
|
458
|
+
search backend returns for one query. `portage index` gives `find` a
|
|
459
|
+
standing, local list of stores and products to route queries to instead,
|
|
460
|
+
built from sources you can read (`portage index sources`):
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
portage index build # every default source
|
|
464
|
+
portage index build --sources shopify_catalog,stores_file
|
|
465
|
+
portage index build --queries queries.txt # one query per line, instead of the built-in taxonomy sweep
|
|
466
|
+
portage index refresh # re-verify entries older than 7 days, add new ones
|
|
467
|
+
portage index show --stores --json
|
|
468
|
+
portage index show --products --json
|
|
469
|
+
portage index add https://some-shop.example
|
|
470
|
+
portage index remove some-shop.example
|
|
471
|
+
portage index sources # name, what each fetches, source file path
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Stored at `~/.portage/index/{stores,products}.json`, **never in git** and
|
|
475
|
+
**never containing a price or stock field** — those are always fetched live.
|
|
476
|
+
Each new origin gets exactly one `/.well-known/ucp` probe (capped at 500 new
|
|
477
|
+
probes per run), throttled, with progress output. Sources:
|
|
478
|
+
|
|
479
|
+
| Source | Fetches | Default |
|
|
480
|
+
| --- | --- | --- |
|
|
481
|
+
| `shopify_catalog` | Merchant origins and product identities, one query per top-level taxonomy node, from `catalog.shopify.com`'s open catalog | on |
|
|
482
|
+
| `stores_file` | Your own `~/.portage/stores.yml` | on |
|
|
483
|
+
| `browser` | Whatever `portage browser import` (below) already saved — this source itself never reads a browser | on, but yields nothing unless you've run `browser import` |
|
|
484
|
+
| `wikidata` | Retailers'/brands' official sites via a public SPARQL query | opt-in (`--sources wikidata`) |
|
|
485
|
+
| `webmcp_sweep` | Which WebMCP preset an origin matches, when a bridge is attached | opt-in, needs a bridge |
|
|
486
|
+
|
|
487
|
+
**The index is untrusted data, on the same footing as any other `find`
|
|
488
|
+
candidate.** It never feeds `Policy#merchant_allowlist` and never counts as
|
|
489
|
+
"you picked a store" for `--yes` — a search ranker (which an index entry
|
|
490
|
+
still is) never gets to complete a purchase on its own.
|
|
491
|
+
|
|
492
|
+
**Known-stores, fetched, not built by you.** The repo itself publishes
|
|
493
|
+
`known-stores/{stores,products}.json` — built the same way, just by the
|
|
494
|
+
maintainer — over jsdelivr's `@main` CDN. `find` uses it automatically
|
|
495
|
+
(cached at `~/.portage/index/known-*.json`, refreshed on `index
|
|
496
|
+
build`/`refresh` or when `doctor` sees it's more than 7 days stale) even
|
|
497
|
+
before you ever run `index build` yourself; your own entries always win over
|
|
498
|
+
it on a conflict. `index build --export DIR` writes a PR-ready copy with
|
|
499
|
+
personal (browser-derived) entries stripped, for anyone who wants to
|
|
500
|
+
contribute a store they found to the shared list.
|
|
501
|
+
|
|
502
|
+
### Browser import (Tier A)
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
portage browser import --dry-run --json
|
|
506
|
+
portage browser import --browser chrome --history-days 90 --max-probes 200
|
|
507
|
+
portage browser import --yes --exclude some-domain-you-declined.example
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Reads your browser's bookmarks and history — Chromium family's `History`
|
|
511
|
+
(SQLite)/`Bookmarks` (JSON), Firefox's `places.sqlite`, Safari's
|
|
512
|
+
`History.db`/`Bookmarks.plist` (needs Full Disk Access on macOS; the command
|
|
513
|
+
explains the prompt and never works around it) — reduces every row to a bare
|
|
514
|
+
domain, and decides each one locally first against the hand-off-only list,
|
|
515
|
+
the local index and the known-stores cache before spending one of at most
|
|
516
|
+
`--max-probes` (default 200) `/.well-known/ucp` probes on an unknown one.
|
|
517
|
+
Kept domains are classified (page titles, bookmark folder names, URL slugs)
|
|
518
|
+
into the same categories `find` routes by, weighted by visit count.
|
|
519
|
+
**Nothing is saved without your approval:** a dry run (or any non-interactive
|
|
520
|
+
run without `--yes`) only shows what it *would* keep; `--yes` (after you've
|
|
521
|
+
reviewed the list, optionally with `--exclude host,host` for ones you don't
|
|
522
|
+
want) writes them to the local index as `sources: ["history"]`/`["bookmark"]`
|
|
523
|
+
entries. `--include-product-pages` (off by default) also keeps product page
|
|
524
|
+
titles/URLs as product entries.
|
|
525
|
+
|
|
526
|
+
**Never reads cookies, saved passwords, or autofill data**, on any browser —
|
|
527
|
+
only the two files named above, verified by a spec that opens a fixture
|
|
528
|
+
profile full of `Login Data`/`Cookies`/`Web Data` decoys and asserts none of
|
|
529
|
+
them were touched.
|
|
530
|
+
|
|
531
|
+
### Portage browser profile (Tier B)
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
portage browser profile init # create the dedicated profile directory
|
|
535
|
+
portage browser profile open # launch it, remote debugging on
|
|
536
|
+
portage browser profile status
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
A dedicated Chromium-family profile (Chrome, Edge, Brave or Arc — Firefox
|
|
540
|
+
and Safari aren't supported for driving) under `~/.portage/browser/`,
|
|
541
|
+
launched with remote debugging scoped to *that* profile only — never your
|
|
542
|
+
default one; Chrome 136+ refuses remote debugging on the default profile
|
|
543
|
+
anyway. Sign into your shopping sites there once. With
|
|
544
|
+
`portage buy ... --handoff-target profile`, the cart is built in this same
|
|
545
|
+
browser via WebMCP (when the store supports it) and the checkout opens there
|
|
546
|
+
for you to pay — driving is limited to a domain allowlist (the store being
|
|
547
|
+
bought from, plus its checkout host); navigating anywhere else stops the
|
|
548
|
+
run. Payment is filled by the browser's own saved-card autofill, triggered
|
|
549
|
+
by your own gesture — Portage never touches a payment field and never
|
|
550
|
+
clicks pay. Requires `gem install portage-ucp-webmcp`.
|
|
551
|
+
|
|
552
|
+
### Retailer offer sources
|
|
553
|
+
|
|
554
|
+
Official, opt-in buyer-side APIs that add more real offers to `portage
|
|
555
|
+
find`, each gated on its own key set in `~/.portage/.env` (or via `portage
|
|
556
|
+
setup`, below):
|
|
557
|
+
|
|
558
|
+
| Retailer | Env var |
|
|
559
|
+
| --- | --- |
|
|
560
|
+
| Walmart Affiliate API | `WALMART_AFFILIATE_API_KEY` |
|
|
561
|
+
| eBay Browse API (Buy It Now only) | `EBAY_BROWSE_ACCESS_TOKEN` (optional `EBAY_MARKETPLACE_ID`) |
|
|
562
|
+
| Best Buy Products API | `BESTBUY_API_KEY` |
|
|
563
|
+
| Etsy Open API v3 (buyer-side `findAllListingsActive`) | `ETSY_LISTINGS_API_KEY` |
|
|
564
|
+
| Amazon Creators API | `AMAZON_CREATORS_ACCESS_TOKEN` (optional `AMAZON_CREATORS_MARKETPLACE`) |
|
|
565
|
+
|
|
566
|
+
With none set, `find` behaves exactly as it did before these existed. Every
|
|
567
|
+
offer from any of these five still ends in hand-off — none of them has a
|
|
568
|
+
checkout `portage buy` can drive, so walmart.com/ebay.com/bestbuy.com are
|
|
569
|
+
hand-off only unconditionally and Amazon/Etsy follow the rules in "Hand-off
|
|
570
|
+
targets and hand-off-only hosts" above (Etsy only when *your own* seller
|
|
571
|
+
credentials aren't already configured for `portage-ucp-etsy`). `portage
|
|
572
|
+
doctor` reports which of the five are active.
|
|
573
|
+
|
|
574
|
+
### The `portage setup` wizard
|
|
575
|
+
|
|
576
|
+
On a TTY, `portage setup` (or `portage doctor`/`portage configure` when
|
|
577
|
+
nothing is configured yet) walks through setup interactively, one skippable
|
|
578
|
+
step at a time, never echoing a secret back: shipping address, search API
|
|
579
|
+
keys, retailer offer source keys, the agent profile, browser import, index
|
|
580
|
+
build, spending policy caps, and hand-off target/hand-off-only hosts. Each
|
|
581
|
+
step delegates to the real command it configures — nothing is
|
|
582
|
+
reimplemented — so it behaves exactly like running that command yourself.
|
|
583
|
+
Under `--json`, or with no TTY on stdin (piped, CI, or a tool call), `setup`
|
|
584
|
+
never prompts: it prints exactly `doctor --json`'s read-only report.
|
|
585
|
+
|
|
349
586
|
### Orders reconcile
|
|
350
587
|
|
|
351
588
|
Nearly every real checkout `portage buy` can't finish itself hands the
|
|
@@ -405,8 +642,9 @@ that).
|
|
|
405
642
|
### Doctor
|
|
406
643
|
|
|
407
644
|
```bash
|
|
408
|
-
portage doctor #
|
|
645
|
+
portage doctor # alias: portage configure
|
|
409
646
|
portage doctor --json
|
|
647
|
+
portage setup # same report, but interactive on a TTY — see below
|
|
410
648
|
```
|
|
411
649
|
|
|
412
650
|
Checks this machine's setup without touching the network (apart from
|
|
@@ -427,6 +665,12 @@ installed, then lists anything that needs fixing:
|
|
|
427
665
|
- `env_file`: which env file was loaded (see "Environment file" below).
|
|
428
666
|
Warns when other users can read it.
|
|
429
667
|
- The confidence gate's backend, the User-Agent, and proxy settings.
|
|
668
|
+
- `index`: local and known-stores index counts and staleness (see "Local
|
|
669
|
+
store index" below).
|
|
670
|
+
- `handoff`: the current `--handoff-target` default and the hand-off-only
|
|
671
|
+
host list, plus the as-is/no-warranty disclaimer.
|
|
672
|
+
- `retailer_offer_sources`: which of the five retailer offer source keys are
|
|
673
|
+
set, and a reminder that none of them can complete a purchase.
|
|
430
674
|
- Seller-side checks against `Portage::Ucp.configuration` (authenticator,
|
|
431
675
|
rate limiter, signing keys, payment handlers). These only run when you
|
|
432
676
|
pass `--require` with your app's initializer (Rails:
|
|
@@ -511,6 +755,50 @@ hand-off. `token` isn't implemented yet; it reports
|
|
|
511
755
|
`webmcp_token_unsupported` rather than attempting completion. Requires
|
|
512
756
|
`gem install portage-ucp-webmcp` — not a hard dependency of `portage-cli`.
|
|
513
757
|
|
|
758
|
+
Against a page whose tools aren't a known platform preset, `Buy` falls back
|
|
759
|
+
to a schema-matched, shopper-confirmed mapping instead of giving up (see
|
|
760
|
+
`portage-ucp-webmcp`'s README, "Stores that don't run Portage"). A mutating
|
|
761
|
+
match prompts on a real TTY with `--json` off. Under `--json`, or with no
|
|
762
|
+
TTY, it stops instead: outcome `webmcp_mapping_unconfirmed`, with the
|
|
763
|
+
proposal in `tool_names_proposal`.
|
|
764
|
+
|
|
765
|
+
No flag passes a mapping back. From the CLI, re-run the same command in
|
|
766
|
+
your own terminal without `--json` and answer the prompt. `--dry-run` is
|
|
767
|
+
enough, because the mapping is confirmed before the dry-run check. The
|
|
768
|
+
approved mapping is saved to `~/.portage/webmcp_mappings.json`, and later
|
|
769
|
+
runs reuse it with no prompt.
|
|
770
|
+
|
|
771
|
+
`Buy` has no `tool_names:` keyword either. A library caller has two hooks.
|
|
772
|
+
`webmcp_mapping_confirm:` takes any object whose `call(proposal, tools)`
|
|
773
|
+
returns a `tool_names:` hash, or nil to stop with
|
|
774
|
+
`webmcp_mapping_unconfirmed`. `Buy` saves whatever hash it returns to
|
|
775
|
+
`webmcp_mappings:`, the store approved mappings are read from (default: a
|
|
776
|
+
`Portage::Cli::WebmcpMappings` on `~/.portage/webmcp_mappings.json`).
|
|
777
|
+
Outside `Buy`, pass `tool_names:` to `Portage::Ucp::WebMcp.connect`
|
|
778
|
+
yourself.
|
|
779
|
+
|
|
780
|
+
`dry_run: true` against a page whose preset hands off through its own
|
|
781
|
+
checkout tool (Shopify's `proceed_to_checkout`) stops after the read-only
|
|
782
|
+
product search: nothing is added to the store's cart, the tab isn't sent to
|
|
783
|
+
checkout and nothing is autofilled. The `dry_run` report carries a `would:`
|
|
784
|
+
key with the line item, the hand-off tool and whether autofill would run.
|
|
785
|
+
|
|
786
|
+
Once the flow hands off to the store's own checkout page, the shopper can
|
|
787
|
+
opt into having it pre-filled: `--autofill`, or
|
|
788
|
+
`PORTAGE_WEBMCP_AUTOFILL=approve` / config.json's `"webmcp_autofill":
|
|
789
|
+
"approve"` (only that literal string turns it on — a generic truthy value
|
|
790
|
+
doesn't). Even opted in, nothing is typed until a second prompt shows the
|
|
791
|
+
shopper exactly which fields and values are about to be entered and they
|
|
792
|
+
approve it — refused outright under `--json` or no TTY. It only ever
|
|
793
|
+
touches contact email and shipping address (from `PORTAGE_SHIP_*`/the new
|
|
794
|
+
`PORTAGE_SHIP_EMAIL`) plus the cheapest shipping rate it can find; it never
|
|
795
|
+
touches a payment field and never clicks submit/pay, and the run still
|
|
796
|
+
always ends in the same `express_stop` hand-off. A headless browser (or one
|
|
797
|
+
that never says) reports `autofill_needs_headed_browser`; a CAPTCHA/
|
|
798
|
+
challenge on the page reports `autofill_blocked` — see
|
|
799
|
+
`portage-ucp-webmcp`'s README, "Approved autofill of the store's checkout",
|
|
800
|
+
for the full field/outcome list.
|
|
801
|
+
|
|
514
802
|
### Decisions
|
|
515
803
|
|
|
516
804
|
`portage buy` and `portage find` make their judgment calls
|
|
@@ -542,6 +830,7 @@ with the same value, as `[outcome]`.
|
|
|
542
830
|
| `policy_blocked` | Your spend policy denied it; `decisions.policy.reason` says why. | yes |
|
|
543
831
|
| `low_confidence` | The confidence gate held it. | yes |
|
|
544
832
|
| `permission_denied` | The store doesn't let this agent complete checkout. | yes |
|
|
833
|
+
| `handoff_only` | Tier C: Amazon or another hand-off-only host. `legal_notice` explains why; see "Hand-off targets and hand-off-only hosts". | yes (built, never fetched) |
|
|
545
834
|
| `store_refused` | The store refused a cart/checkout call (e.g. sold out). | when the store gave one |
|
|
546
835
|
| `no_match` | Nothing in the store's results matched. | no |
|
|
547
836
|
| `browse_only` | The store has a catalog but no UCP checkout. | when an adapter offers a link |
|
|
@@ -708,7 +997,8 @@ already refuses to commit against a merchant.
|
|
|
708
997
|
|
|
709
998
|
| Backend | Credentials | Notes |
|
|
710
999
|
| --- | --- | --- |
|
|
711
|
-
| Allowlist | `~/.portage/stores.yml` (YAML array of URLs) or `PORTAGE_STORES` (comma-separated) | Stores you already trust. Checked first, costs no network call. |
|
|
1000
|
+
| Allowlist | `~/.portage/stores.yml` (YAML array of URLs, optionally tagged `{url:, categories: [...]}`) or `PORTAGE_STORES` (comma-separated, untagged) | Stores you already trust. Checked first, costs no network call. Tagged entries are routed by category (see "Categories and routing" above); untagged ones are always a candidate. |
|
|
1001
|
+
| Index | `~/.portage/index/` (your own `portage index build`) plus the repo's published known-stores list | Ranked between Allowlist and DuckDuckGo. Sits out entirely until an index actually exists — a fresh install's behavior is unchanged. Also matches a query against an indexed *product* by name or GTIN, not just a store. |
|
|
712
1002
|
| DuckDuckGo | none | The [Instant Answer API](https://api.duckduckgo.com/api). Answers *entity* queries, not web queries: `burton snowboards` resolves to burton.com, `snowboard` resolves to nothing. |
|
|
713
1003
|
| Brave | `BRAVE_SEARCH_API_KEY` | Real web results. Set this up if you want open-ended queries to work. |
|
|
714
1004
|
| Google | `GOOGLE_CSE_KEY` + `GOOGLE_CSE_CX` | Programmable Search JSON API. |
|
|
@@ -717,6 +1007,13 @@ Backends that have no credentials sit out; DuckDuckGo is the keyless default
|
|
|
717
1007
|
because it's the only no-key engine with a real API, and its narrowness is the
|
|
718
1008
|
price of not scraping.
|
|
719
1009
|
|
|
1010
|
+
Separate from all of the above, `portage find`/`buy` also merge in offers
|
|
1011
|
+
directly from `OfferSources` — `ShopifyCatalog` (no key,
|
|
1012
|
+
`catalog.shopify.com`'s open catalog, always on) and the retailer offer
|
|
1013
|
+
sources (opt-in, keyed — see "Retailer offer sources" above). These skip the
|
|
1014
|
+
origin-probe step entirely, since a catalog result already names the
|
|
1015
|
+
merchant's own product page.
|
|
1016
|
+
|
|
720
1017
|
### Console
|
|
721
1018
|
|
|
722
1019
|
`portage-console` is a separate executable — a read-only IRB REPL over the
|