@finchtech/cli 0.3.8 → 0.3.9

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.
Files changed (3) hide show
  1. package/README.md +136 -132
  2. package/dist/finch.js +82 -61
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,132 +1,136 @@
1
- # Finch Agent CLI
2
-
3
- Install the public npm package with `npm install -g @finchtech/cli`, then run `finch --help`. The primary executable is `finch`; `finchtech` is an equivalent fallback for machines where another application already owns the `finch` command.
4
-
5
- The Finch Agent CLI stores wallet keys, AgentCLI Session bearers, and MCP grant bearers only in an owner-only `FINCHTECH_HOME`, which defaults to `~/.finchtech`; secrets are never accepted as command arguments or emitted to stdout.
6
-
7
- For an existing private key, run `finch wallet use --file <PATH>` on a fresh installation, then `finch login --help` to choose an authentication chain. The file must contain a single 0x-prefixed 32-byte hex private key and have owner-only permissions. First import is offline and makes that wallet current. `finch wallet create` generates a new identity instead. An existing Account switch requires login and remote revocation before changing the current wallet. Incomplete wallet files require recovery from backup; residual local authorization requires restoring the original wallet or explicitly clearing local authorization with `finch logout --local`.
8
-
9
- `finch logout` revokes Account-bound remote authority before removing local Session and grant state. If invalid, corrupt, or legacy Session metadata prevents that flow, `finch logout --local` performs an explicit offline reset of only `session/` and `grants/`. It makes no network request and reports `remoteRevoked: false`; it remains available while an irreversible recovery is pending and preserves profiles, registered wallets, transaction journals, and recovery records so the user can log in again and run the recorded recovery command. Ordinary logout and wallet switching remain blocked until that recovery completes.
10
-
11
- CLI-authored EVM addresses accept canonical lowercase or valid EIP-55 input and are normalized to lowercase before request validation and transport. Invalid mixed-case and uppercase forms are rejected. Remote responses and Remote MCP plans remain strictly lowercase so normalization cannot change signed or hashed plan bytes.
12
-
13
- Valid remote Identity, Agent, Task, Skill, and OAuth errors are emitted in their original domain envelope with the remote trace ID. Local transport failures use `CLI_REMOTE_UNAVAILABLE`; malformed or contract-invalid responses use `CLI_RESPONSE_INVALID`. Locally synthesized failures use a null trace ID rather than impersonating a server trace.
14
-
15
- Skill upload failures also retain the final `uploadBytes`, `uploadObjectKind`, HTTP `remoteStatus` (null when no response was obtained), and available allowlisted `remoteCode`, `remoteRequestId`, and `remoteTraceId` in the JSON-encoded `state`. Locally synthesized errors retain the existing null `reasonCode` and `traceId` contract; downstream evidence is labelled explicitly in `state`. These fields never include raw response text, upload filenames, credentials, or transport causes. `not_attempted` refers only to chain submission: storage may have accepted an object before a response was lost. Follow `contact_support` until the individual attempt is reconciled; another publication's status is not evidence about this attempt.
16
-
17
- ZIP import failures return `AGENT_MARKET_REQUEST_INVALID`, exit code `2`, and `fix_request` before upload. The JSON-encoded `state` identifies `SKILL_SOURCE_ZIP_INVALID` and a `zipReason`: `archive_too_large`, `expanded_content_too_large`, `utf8_flag_missing`, `filename_encoding_invalid`, or `invalid_archive`. Size failures include `actualBytes`, `maxBytes` (10,485,760), and `sizeBasis`: `archive` measures the compressed input; `declared_expanded` is the cumulative uncompressed size declared by entries at the first limit violation, before decompression. A safely decoded root-relative entry `path` is included when available. Non-ASCII filenames require valid UTF-8 bytes and bit 11 in the ZIP headers; rebuilding with those flags preserves file contents. These import limits are independent of final upload admission.
18
-
19
- Intent HTTP failures include `remotePath: "/api/skills/intents"`, `remoteStatus`, and available safe correlation IDs. `responseIssue` distinguishes `transport_failed`, `body_read_failed`, `empty_body`, `invalid_content_length`, `response_too_large`, `invalid_json`, and `schema_mismatch`. Schema failures include at most eight `schemaIssues` with known field paths and validation keywords; an empty path means the response root or an unrecognized field, whose name is withheld. No response values, arbitrary property names, URLs, or raw causes are printed. Malformed success responses are rejected during `intent`, before signing or writing a pending-send marker. Use the returned recovery command for the same publication; the CLI never automatically retries intent requests or resubmits transactions. The fingerprint identifies the local recovery journal, not a server trace; use returned remote IDs to correlate server logs.
20
-
21
- The CLI measures the exact final multipart request, including metadata and framing. At or below 4,000,000 bytes it posts that request to the inline upload route. Above it the object goes through the staged transport instead: the CLI asks for a signed URL, writes the bytes straight to storage, and has the server read them back, verify them and pin them, so the platform's 4,500,000-byte request-body limit does not apply. These MB thresholds are decimal. The per-object ceilings are enforced against the final object either way: 30 MiB (31,457,280 bytes) for a package, 5 MiB for a cover, and 3 MiB (3,145,728 bytes) for a manifest. The package ceiling is one shared constant, applied by upload admission, by the retrieval route, by this client before it uploads, and by this client when it downloads, so a package that publishes is a package that reads back. Repacking a compressed source ZIP may increase its size. Passing admission does not certify production upload success or authorize replaying a previously failed publication.
22
-
23
- The command groups are `wallet`, `login/status/logout`, `site`, `mcp doctor`, `mcp authorize`, `mcp grant`, `creator`, `credentials`, `intent`, `operation show`, `purchase`, `task`, and `skill`. Run `finch --help` for the exact command list as one JSON value.
24
-
25
- `finch site open` creates a sixty-second one-use handoff from the active AgentCLI Session and opens the exact configured Finch origin in the local default Browser. `finch site login <REQUEST_URL>` binds a website-created five-minute request once, then opens the same confirmation page. Both commands require the wallet, CLI, and graphical Browser on the user's machine. A launch failure returns `BROWSER_OPEN_FAILED` without printing the secret completion URL. The Browser receives only a normal seven-day Finch Web Session; wallet signing, broadcast, and market confirmation remain local CLI actions.
26
-
27
- `finch mcp doctor` is read-only. It returns a `checks` object for endpoint, wallet, local Session, remote Session authority, and MCP discovery. Each check reports `ok`, `missing`, `invalid`, `unavailable`, or `skipped`, with guidance for unhealthy checks. Missing login or wallet state does not prevent independent discovery checks; an invalid endpoint prevents network checks. The command emits one JSON report with exit code 0 when ready, or 2 when any required check is unhealthy. It verifies that the active CLI authority, AgentCLI Session, local wallet, MCP challenge, OAuth metadata, and source-controlled persistent-Harness manifest agree. The Agent Harness owns Remote MCP configuration, OAuth credentials, and refresh; the CLI does not configure or launch Codex and never reads Harness OAuth material. During the Harness's native OAuth flow, the authorization page supplies an exact `finch mcp authorize <REQUEST_TOKEN>` command. The versioned base64url token carries the original OAuth request without shell metacharacters so Windows package-manager and Volta launchers cannot split it; the CLI decodes it before applying the existing origin, callback, state, and scope checks. That command uses only the active local AgentCLI Session and opens the verified callback without returning its one-use code. It never uses a Browser Session, Social account, or extension wallet as Agent authority. Configure `https://www.finchtech.ai/mcp` once through the Harness, then use MCP `identity_actor_get` together with `finch status` to verify exact Account and wallet alignment before custody. The server-reported environment is diagnostic and never selects the CLI endpoint or a transaction chain.
28
-
29
- Doctor's `ready: true` is scoped by `readinessScope: "cli_session_and_mcp_discovery"`. Its `harnessOAuth.status` is always `not_checked`: CLI login and valid discovery metadata do not establish that the connected Harness has usable OAuth credentials, a saved issuer, or working refresh. The result includes identity-verification and native reauthorization guidance. If the Harness reports an OAuth failure, complete its native MCP authorization flow and the authorization page's fresh `finch mcp authorize` command, then verify MCP identity again.
30
-
31
- Run `finch login --help` to list supported production authentication chain IDs (1, 10, 56, 8453, 42161). For example, `finch login --chain-id 56` selects BNB Smart Chain for authentication only.
32
-
33
- Ordinary installs require no profile file or environment selection. Network commands default to the PROD endpoint at `https://www.finchtech.ai`. `finch login --chain-id <CHAIN_ID>` explicitly selects only the authentication chain, which the selected server must support. Market transaction chains come from Remote MCP/API plans and are independent of the login chain. `intent approve` signs only the shared publication-v2 or Local-purchase-v1 EIP-712 authorization; it cannot sign arbitrary transaction calldata.
34
-
35
- `operation show` and signing-decision recovery require an active, unexpired MCP grant for the current account and endpoint with `agent_market:operations:read`. The CLI selects that grant from protected local state and never substitutes the AgentCLI Session bearer.
36
-
37
- Creator commands are thin projections of the shared Agent Market merchant operations. Reads require `agent_market:merchant:read`; mutations require `agent_market:merchant:write`, a stable caller-chosen `--request-id` UUID for local idempotency recovery, and `--expected-revision` wherever the shared operation requires If-Match. Structured request bodies come from bounded JSON `--from-file` documents and are validated against the same shared schemas before HTTP.
38
-
39
- Local AgentOn HMAC generation is not a product command. `credentials material-download <setupId> --out-file <absolute-path>` claims the AgentOn setup, downloads only its exact live lease as raw FCR1, and creates a new owner-only file; stdout contains only `keyId` and `path`. `credentials fulfill` accepts only Direct credential uploads. It accepts either an existing protected FCR1 with `--from-file <path>`, or a protected raw Bearer token with `--bearer-token-file <path> --key-id <keyId>`; the latter validates one whitespace-free token and adds the `Bearer ` prefix only in CLI memory. Secret bytes never enter argv or stdout.
40
-
41
- `purchase transaction <operationId>` is read-only. It requires an active `agent_market:buyer:purchase` MCP grant and returns the two server-composed transaction requests plus their chain authority; it does not sign or broadcast arbitrary calldata.
42
-
43
- `purchase submit <operationId>` requires the same grant and uses only the source-controlled public RPCs for the plan's chain. It uses the protected wallet to re-encode and submit the exact server-composed USDC approval and AgentService Vault purchase. Before signing it verifies `eth_chainId`, the reviewed chain contract authority, every transaction field, and a successful `eth_call` simulation. Every returned broadcast hash is atomically checkpointed before receipt waiting; a restart re-verifies the exact wallet, call, receipt, block, and current server authority and never re-broadcasts a known phase. Finch then verifies the canonical confirmation event and creates Call Rights. If the purchase receipt conclusively reverts, the paired zero-allowance cleanup follows the same checkpoint discipline; unknown receipts are never guessed. The RPC owns nonce selection. User RPC and environment variables are ignored, and neither RPC URLs nor wallet keys are emitted.
44
-
45
- `purchase recover-approval <operationId> --transaction-hash <hash>` is the narrow adoption path for an approval broadcast before its durable checkpoint was written. It re-fetches the current server authority and active wallet, then verifies the exact successful approval transaction and sufficient live token allowance on the plan chain before writing one owner-only recovery journal. It never broadcasts, confirms, scans chain history, or accepts a nonce. A subsequent `purchase submit` continues with the purchase phase without approving again.
46
-
47
- If the process exits after broadcasting the Vault purchase but before confirmation returns, recover with `purchase confirm <operationId> --transaction-hash <hash>`. This command never trusts the supplied hash: Finch fetches and decodes the canonical chain receipt before any Call Rights mutation.
48
-
49
- Task and Skill business discovery and preparation stay in the Remote MCP server. The `task` commands only fund an exact prepared Task pool, sign exact prepared awards, or submit an exact prepared reclaim. Skill purchase, conversion, price, and supply commands submit strict MCP preparations. `skill publish-submit --path <file-or-directory> --from-file <plan>` and `skill version-submit` accept either one file or one directory, then apply the plan's independent `plaintext`, `finchip_v2`, or `lit` content mode; filtering, deterministic ZIP construction, encryption, upload, and key custody remain local. `skill asset-upload --path <cover> --from-file <plan>` uploads a separately prepared public cover, and `skill presentation-submit --from-file <plan>` publishes bounded public presentation metadata without changing protected package bytes. `skill download --from-file <delivery> --out-file <new-file>` requires an active AgentCLI Session for both plaintext and protected content. It retrieves packages through the authenticated delivery gateway, signs with the entitled local wallet for protected content, verifies the SkillRoot and every file (or the plaintext ZIP hash for historical Skills), acknowledges the verified download, and then creates an owner-only file. CLI 0.3.8 or newer is required; an acknowledgement failure aborts before output-file creation. Every transaction action uses its plan's chain, source-controlled public RPCs, and reviewed chain-level contract authority. The CLI recomputes transaction bytes locally and rejects mismatched chains, destinations, calldata, values, source commits, or envelope tags.
50
-
51
- ## ZIP import and SkillRoot publication
52
-
53
- Version 0.3.1 fixes Skill search continuation: pass the returned `nextCursor` unchanged as
54
- `--cursor`. Search uses the server rules; the CLI does not implement a matching algorithm.
55
- Older clients receive an upgrade message from the Skill search API.
56
-
57
- Version 0.3.0 upgrades Skill publication and encrypted delivery together. Upgrade with
58
- `pnpm add -g @finchtech/cli@0.3.8` and verify `finch --version` before publishing new Skills
59
- or downloading encrypted Skills from the upgraded gateway. Older CLI versions cannot use
60
- the new encrypted delivery, including for historical Skills; 0.3.0 retains historical package support.
61
-
62
- A single `.zip` passed to `skill publish-submit --path` or `skill version-submit --path` is expanded locally before filtering and primary-document hashing; request a `file` source plan for this input. Directory inputs keep ZIP assets as ordinary files. Imports support stored/DEFLATE ZIPs up to the package ceiling compressed and expanded, with at most 1024 entries. Unsafe paths, symbolic links, encrypted ZIPs, ZIP64, conflicting entries, and corrupt content are refused. Include `SKILL.md`, `README.md`, or one unambiguous root Markdown document.
63
-
64
- New Skills publish a DAG-CBOR SkillRoot hash in one graph bundle. Existing Skill identities keep ZIP-hash updates. New Skills require this updated CLI; it verifies every object and decrypts protected files locally into an ordinary ZIP. Historical ZIP downloads and whole-package decryption remain supported. A pending publication reuses its uploaded bytes and saved root commitment on resume.
65
-
66
- The [publication ADR](../../docs/adr/ADR-0064-20260908-skillroot-publication.md) specifies the pinned draft, bundle/profile format, compatibility trust boundary, and rollout order.
67
-
68
- Finch-managed and Lit downloads use Oracle V2 device-sealed grants and a non-extractable local CryptoKey.
69
- The updated gateway also supports historical Finch-managed and Lit ZIPs through this delivery. It refuses
70
- old bare-key buyer requests; use the updated CLI. Both providers use the same sealed delivery. See
71
- [ADR-0065](../../docs/adr/ADR-0065-20260908-skill-oracle-v2-delivery.md) for deployment requirements.
72
-
73
- Publication preparation accepts an optional `summary` (up to 500 characters). It is committed in
74
- SkillRoot exactly as supplied, matching Browser publication metadata; omission means an empty summary.
75
- Equal public file bytes, paths, name, summary, version and history produce equal roots. Encrypted
76
- publications use random keys/IVs and need not produce the same root. Updates send the proposed
77
- manifest for server validation; URI intents are prepared after the new chain binding is confirmed.
78
-
79
- Finch-managed publication key requests use a signed timestamp and a one-time nonce. Each request is
80
- valid for five minutes; retrying after expiry or a lost response requires a fresh signature. The
81
- gateway rejects older CLI publication requests without timestamps. Historical downloads are unchanged.
82
-
83
- ### Local publication failures
84
-
85
- Directory publication rejects symbolic links, including internal relative links and broken links.
86
- It skips excluded paths (for example `.git`, `node_modules`, `.aws`, and credential files) before reading
87
- or descending into them. `excluded` lists a skipped directory once instead of enumerating its children.
88
- Keep required resource paths functional when preparing a link-free source; deleting required links is
89
- not a valid repair. ZIP imports also reject symbolic links.
90
-
91
- Source errors keep exit code 2 and `AGENT_MARKET_REQUEST_INVALID`. Parse the JSON string in `error.state`
92
- for `diagnosticCode`: `SKILL_SOURCE_SYMLINK_UNSUPPORTED`, `SKILL_SOURCE_UNREADABLE`, or
93
- `SKILL_UPLOAD_SIZE_INVALID`, `SKILL_SOURCE_ZIP_INVALID`, `SKILL_SOURCE_PRIMARY_DOCUMENT_INVALID`, or `SKILL_MANIFEST_SIZE_INVALID`. Source diagnostics include a root-relative `path` (`.` for the selected
94
- root); size diagnostics include `actualBytes` and `maxBytes`. The internal packaging bound is the package ceiling and applies to the final
95
- upload object, including SkillRoot metadata and encryption overhead. STORE ZIP construction does not
96
- compress files. A size rejection occurs before that upload and before transaction submission.
97
-
98
- Publish, version, and publication-recovery failures also include `phase`, `fingerprint`,
99
- `transactionHashes` (keyed by `primary`, `uri`, or `lit`), `submissionStatus`, and `recoveryCommand` in
100
- `error.state`. `packaging` includes local hashing/encryption; `transaction` covers signing, broadcast,
101
- and receipt handling. A validated remote error keeps its code, message, trace and HTTP status
102
- (`remoteStatus`), with its original state retained as `remoteState`; publication retry guidance takes
103
- precedence over a remote service's generic retry flag. Private causes, RPC URLs, credentials and
104
- journal contents are never printed.
105
-
106
- Publication failures report `retryable` and `nextAction` according to the failure. A projection failure after confirmed transactions can be retryable; inspect `unconfirmedActions` before claiming the complete publication succeeded. This is not permission to replay a paid operation.
107
- `not_attempted` means no send was attempted in this invocation and no hashes were found in the
108
- validated journal; it is not proof about other invocations. `hash_known` includes known transaction
109
- hashes. `uncertain` means sending or durable checkpointing failed. For creation/version/key stages, contact support before retrying or
110
- recovering, even when a hash is shown. A non-null `recoveryCommand` is provided only after a journal
111
- is validated or saved and no submission/checkpoint uncertainty was observed. Recheck CLI/MCP identity
112
- and use that command rather than repeating publish-submit. Cleanup failures do not claim a journal
113
- still exists. If source is unavailable or its excluded-path inventory changed across CLI versions,
114
- the fingerprint-only recovery command can use the already stored publication bytes.
115
-
116
- These fixes require CLI 0.3.3 or newer; npm 0.3.2 does not include them. Updating a Skill does not upgrade the installed CLI. Journal v6 saves a pending stage before signing/submitting. Missing hashes in creating stages of a pending or legacy v4/v5 journal block automatic resubmission; follow the reported chain-state checks and contact support. CLI 0.3.7 permits the URI assignment stage to converge through its typed recovery path. Known hashes are verified without rebroadcast. The pending marker also blocks destructive local authorization/wallet cleanup through the recovery inventory.
117
-
118
- ## Bootstrap and health reports (CLI 0.3.4+)
119
-
120
- First-wallet offline import, installed Account-switch revocation, partial doctor checks and exit code 2, contextual wallet recovery guidance, login authentication-chain help, and strict OAuth UUID response validation require CLI 0.3.4 or newer. Updating the website or Skill does not upgrade an installed CLI.
121
-
122
- ## WebP image detection (CLI 0.3.5+)
123
-
124
- CLI 0.3.5 fixes valid WebP covers and packaged detail images being rejected when binary file-size bytes were decoded as UTF-8. Upgrade the installed CLI to receive the local detection fix; website or Skill updates alone do not update the executable. Image types, size limits, and publication recovery rules are unchanged.
125
-
126
- ## Market category compatibility (CLI 0.3.6+)
127
-
128
- Use CLI 0.3.6 or newer for the expanded market categories. Skill response display categories now accept bounded text, so future taxonomy additions do not require another CLI upgrade. Updating the website or this Skill does not upgrade an installed CLI.
129
-
130
- ## Staged uploads and external media (CLI 0.3.7+)
131
-
132
- CLI 0.3.7 supports authorized external-media manifests and 30 MiB final packages on both upload and download. Browser publishing still uses multipart and refuses external media. Failed staged uploads attempt ticket release and report its result in the sanitized diagnostic. Presentation submission reports progress and whether setting the same URI can safely converge; a stale plan, authorization failure or invalid input still requires correction before retrying. Read the maintained Finch Market Skill for the complete external-host and recovery boundaries.
1
+ # Finch Agent CLI
2
+
3
+ Install the public npm package with `npm install -g @finchtech/cli`, then run `finch --help`. The primary executable is `finch`; `finchtech` is an equivalent fallback for machines where another application already owns the `finch` command.
4
+
5
+ The Finch Agent CLI stores wallet keys, AgentCLI Session bearers, and MCP grant bearers only in an owner-only `FINCHTECH_HOME`, which defaults to `~/.finchtech`; secrets are never accepted as command arguments or emitted to stdout.
6
+
7
+ For an existing private key, run `finch wallet use --file <PATH>` on a fresh installation, then `finch login --help` to choose an authentication chain. The file must contain a single 0x-prefixed 32-byte hex private key and have owner-only permissions. First import is offline and makes that wallet current. `finch wallet create` generates a new identity instead. An existing Account switch requires login and remote revocation before changing the current wallet. Incomplete wallet files require recovery from backup; residual local authorization requires restoring the original wallet or explicitly clearing local authorization with `finch logout --local`.
8
+
9
+ `finch logout` revokes Account-bound remote authority before removing local Session and grant state. If invalid, corrupt, or legacy Session metadata prevents that flow, `finch logout --local` performs an explicit offline reset of only `session/` and `grants/`. It makes no network request and reports `remoteRevoked: false`; it remains available while an irreversible recovery is pending and preserves profiles, registered wallets, transaction journals, and recovery records so the user can log in again and run the recorded recovery command. Ordinary logout and wallet switching remain blocked until that recovery completes.
10
+
11
+ CLI-authored EVM addresses accept canonical lowercase or valid EIP-55 input and are normalized to lowercase before request validation and transport. Invalid mixed-case and uppercase forms are rejected. Remote responses and Remote MCP plans remain strictly lowercase so normalization cannot change signed or hashed plan bytes.
12
+
13
+ Valid remote Identity, Agent, Task, Skill, and OAuth errors are emitted in their original domain envelope with the remote trace ID. Local transport failures use `CLI_REMOTE_UNAVAILABLE`; malformed or contract-invalid responses use `CLI_RESPONSE_INVALID`. Locally synthesized failures use a null trace ID rather than impersonating a server trace.
14
+
15
+ Skill upload failures also retain the final `uploadBytes`, `uploadObjectKind`, HTTP `remoteStatus` (null when no response was obtained), and available allowlisted `remoteCode`, `remoteRequestId`, and `remoteTraceId` in the JSON-encoded `state`. Locally synthesized errors retain the existing null `reasonCode` and `traceId` contract; downstream evidence is labelled explicitly in `state`. These fields never include raw response text, upload filenames, credentials, or transport causes. `not_attempted` refers only to chain submission: storage may have accepted an object before a response was lost. Follow `contact_support` until the individual attempt is reconciled; another publication's status is not evidence about this attempt.
16
+
17
+ ZIP import failures return `AGENT_MARKET_REQUEST_INVALID`, exit code `2`, and `fix_request` before upload. The JSON-encoded `state` identifies `SKILL_SOURCE_ZIP_INVALID` and a `zipReason`: `archive_too_large`, `expanded_content_too_large`, `utf8_flag_missing`, `filename_encoding_invalid`, or `invalid_archive`. Size failures include `actualBytes`, `maxBytes` (10,485,760), and `sizeBasis`: `archive` measures the compressed input; `declared_expanded` is the cumulative uncompressed size declared by entries at the first limit violation, before decompression. A safely decoded root-relative entry `path` is included when available. Non-ASCII filenames require valid UTF-8 bytes and bit 11 in the ZIP headers; rebuilding with those flags preserves file contents. These import limits are independent of final upload admission.
18
+
19
+ Intent HTTP failures include `remotePath: "/api/skills/intents"`, `remoteStatus`, and available safe correlation IDs. `responseIssue` distinguishes `transport_failed`, `body_read_failed`, `empty_body`, `invalid_content_length`, `response_too_large`, `invalid_json`, and `schema_mismatch`. Schema failures include at most eight `schemaIssues` with known field paths and validation keywords; an empty path means the response root or an unrecognized field, whose name is withheld. No response values, arbitrary property names, URLs, or raw causes are printed. Malformed success responses are rejected during `intent`, before signing or writing a pending-send marker. Use the returned recovery command for the same publication; the CLI never automatically retries intent requests or resubmits transactions. The fingerprint identifies the local recovery journal, not a server trace; use returned remote IDs to correlate server logs.
20
+
21
+ The CLI measures the exact final multipart request, including metadata and framing. At or below 4,000,000 bytes it posts that request to the inline upload route. Above it the object goes through the staged transport instead: the CLI asks for a signed URL, writes the bytes straight to storage, and has the server read them back, verify them and pin them, so the platform's 4,500,000-byte request-body limit does not apply. These MB thresholds are decimal. The per-object ceilings are enforced against the final object either way: 30 MiB (31,457,280 bytes) for a package, 5 MiB for a cover, and 3 MiB (3,145,728 bytes) for a manifest. The package ceiling is one shared constant, applied by upload admission, by the retrieval route, by this client before it uploads, and by this client when it downloads, so a package that publishes is a package that reads back. Repacking a compressed source ZIP may increase its size. Passing admission does not certify production upload success or authorize replaying a previously failed publication.
22
+
23
+ The command groups are `wallet`, `login/status/logout`, `site`, `mcp doctor`, `mcp authorize`, `mcp grant`, `creator`, `credentials`, `intent`, `operation show`, `purchase`, `task`, and `skill`. Run `finch --help` for the exact command list as one JSON value.
24
+
25
+ `finch site open` creates a sixty-second one-use handoff from the active AgentCLI Session and opens the exact configured Finch origin in the local default Browser. `finch site login <REQUEST_URL>` binds a website-created five-minute request once, then opens the same confirmation page. Both commands require the wallet, CLI, and graphical Browser on the user's machine. A launch failure returns `BROWSER_OPEN_FAILED` without printing the secret completion URL. The Browser receives only a normal seven-day Finch Web Session; wallet signing, broadcast, and market confirmation remain local CLI actions.
26
+
27
+ `finch mcp doctor` is read-only. It returns a `checks` object for endpoint, wallet, local Session, remote Session authority, and MCP discovery. Each check reports `ok`, `missing`, `invalid`, `unavailable`, or `skipped`, with guidance for unhealthy checks. Missing login or wallet state does not prevent independent discovery checks; an invalid endpoint prevents network checks. The command emits one JSON report with exit code 0 when ready, or 2 when any required check is unhealthy. It verifies the endpoint, wallet/key binding, Account, Session identity, environment, audience and expiry, plus the MCP challenge and required OAuth capabilities. Each remote read has a 15-second timeout. Wallet inspection does not migrate legacy files. Client guide availability, client names, guide versions and repository paths do not affect readiness; `compatibilityManifestUrl` is only an informational link. The Agent Harness owns Remote MCP configuration, OAuth credentials, and refresh; the CLI does not configure or launch the Harness and never reads Harness OAuth material. During the Harness's native OAuth flow, the authorization page supplies an exact `finch mcp authorize <REQUEST_TOKEN>` command. The versioned base64url token carries the original OAuth request without shell metacharacters so Windows package-manager and Volta launchers cannot split it; the CLI decodes it before applying the existing origin, callback, state, and scope checks. That command uses only the active local AgentCLI Session and opens the verified callback without returning its one-use code. It never uses a Browser Session, Social account, or extension wallet as Agent authority. Configure `https://www.finchtech.ai/mcp` once through the Harness, then use MCP `identity_actor_get` together with `finch status` to verify exact Account and wallet alignment before custody. The server-reported environment is diagnostic and never selects the CLI endpoint or a transaction chain.
28
+
29
+ Doctor's `ready: true` is scoped by `readinessScope: "cli_session_and_mcp_discovery"`. Its `harnessOAuth.status` is always `not_checked`: CLI login and valid discovery metadata do not establish that the connected Harness has usable OAuth credentials, a saved issuer, or working refresh. The result includes identity-verification and native reauthorization guidance. If the Harness reports an OAuth failure, complete its native MCP authorization flow and the authorization page's fresh `finch mcp authorize` command, then verify MCP identity again.
30
+
31
+ Run `finch login --help` to list supported production authentication chain IDs (1, 10, 56, 8453, 42161). For example, `finch login --chain-id 56` selects BNB Smart Chain for authentication only.
32
+
33
+ Ordinary installs require no profile file or environment selection. Network commands default to the PROD endpoint at `https://www.finchtech.ai`. `finch login --chain-id <CHAIN_ID>` explicitly selects only the authentication chain, which the selected server must support. Market transaction chains come from Remote MCP/API plans and are independent of the login chain. `intent approve` signs only the shared publication-v2 or Local-purchase-v1 EIP-712 authorization; it cannot sign arbitrary transaction calldata.
34
+
35
+ `operation show` and signing-decision recovery require an active, unexpired MCP grant for the current account and endpoint with `agent_market:operations:read`. The CLI selects that grant from protected local state and never substitutes the AgentCLI Session bearer.
36
+
37
+ Creator commands are thin projections of the shared Agent Market merchant operations. Reads require `agent_market:merchant:read`; mutations require `agent_market:merchant:write`, a stable caller-chosen `--request-id` UUID for local idempotency recovery, and `--expected-revision` wherever the shared operation requires If-Match. Structured request bodies come from bounded JSON `--from-file` documents and are validated against the same shared schemas before HTTP.
38
+
39
+ Local AgentOn HMAC generation is not a product command. `credentials material-download <setupId> --out-file <absolute-path>` claims the AgentOn setup, downloads only its exact live lease as raw FCR1, and creates a new owner-only file; stdout contains only `keyId` and `path`. `credentials fulfill` accepts only Direct credential uploads. It accepts either an existing protected FCR1 with `--from-file <path>`, or a protected raw Bearer token with `--bearer-token-file <path> --key-id <keyId>`; the latter validates one whitespace-free token and adds the `Bearer ` prefix only in CLI memory. Secret bytes never enter argv or stdout.
40
+
41
+ `purchase transaction <operationId>` is read-only. It requires an active `agent_market:buyer:purchase` MCP grant and returns the two server-composed transaction requests plus their chain authority; it does not sign or broadcast arbitrary calldata.
42
+
43
+ `purchase submit <operationId>` requires the same grant and uses only the source-controlled public RPCs for the plan's chain. It uses the protected wallet to re-encode and submit the exact server-composed USDC approval and AgentService Vault purchase. Before signing it verifies `eth_chainId`, the reviewed chain contract authority, every transaction field, and a successful `eth_call` simulation. Every returned broadcast hash is atomically checkpointed before receipt waiting; a restart re-verifies the exact wallet, call, receipt, block, and current server authority and never re-broadcasts a known phase. Finch then verifies the canonical confirmation event and creates Call Rights. If the purchase receipt conclusively reverts, the paired zero-allowance cleanup follows the same checkpoint discipline; unknown receipts are never guessed. The RPC owns nonce selection. User RPC and environment variables are ignored, and neither RPC URLs nor wallet keys are emitted.
44
+
45
+ `purchase recover-approval <operationId> --transaction-hash <hash>` is the narrow adoption path for an approval broadcast before its durable checkpoint was written. It re-fetches the current server authority and active wallet, then verifies the exact successful approval transaction and sufficient live token allowance on the plan chain before writing one owner-only recovery journal. It never broadcasts, confirms, scans chain history, or accepts a nonce. A subsequent `purchase submit` continues with the purchase phase without approving again.
46
+
47
+ If the process exits after broadcasting the Vault purchase but before confirmation returns, recover with `purchase confirm <operationId> --transaction-hash <hash>`. This command never trusts the supplied hash: Finch fetches and decodes the canonical chain receipt before any Call Rights mutation.
48
+
49
+ Task and Skill business discovery and preparation stay in the Remote MCP server. The `task` commands only fund an exact prepared Task pool, sign exact prepared awards, or submit an exact prepared reclaim. Skill purchase, conversion, price, and supply commands submit strict MCP preparations. `skill publish-submit --path <file-or-directory> --from-file <plan>` and `skill version-submit` accept either one file or one directory, then apply the plan's independent `plaintext`, `finchip_v2`, or `lit` content mode; filtering, deterministic ZIP construction, encryption, upload, and key custody remain local. `skill asset-upload --path <cover> --from-file <plan>` uploads a separately prepared public cover, and `skill presentation-submit --from-file <plan>` publishes bounded public presentation metadata without changing protected package bytes. `skill download --from-file <delivery> --out-file <new-file>` requires an active AgentCLI Session for both plaintext and protected content. It retrieves packages through the authenticated delivery gateway, signs with the entitled local wallet for protected content, verifies the SkillRoot and every file (or the plaintext ZIP hash for historical Skills), acknowledges the verified download, and then creates an owner-only file. CLI 0.3.8 or newer is required; an acknowledgement failure aborts before output-file creation. Every transaction action uses its plan's chain, source-controlled public RPCs, and reviewed chain-level contract authority. The CLI recomputes transaction bytes locally and rejects mismatched chains, destinations, calldata, values, source commits, or envelope tags.
50
+
51
+ ## ZIP import and SkillRoot publication
52
+
53
+ Version 0.3.1 fixes Skill search continuation: pass the returned `nextCursor` unchanged as
54
+ `--cursor`. Search uses the server rules; the CLI does not implement a matching algorithm.
55
+ Older clients receive an upgrade message from the Skill search API.
56
+
57
+ Version 0.3.0 upgrades Skill publication and encrypted delivery together. Upgrade with
58
+ `pnpm add -g @finchtech/cli@0.3.9` and verify `finch --version` before publishing new Skills
59
+ or downloading encrypted Skills from the upgraded gateway. Older CLI versions cannot use
60
+ the new encrypted delivery, including for historical Skills; 0.3.0 retains historical package support.
61
+
62
+ A single `.zip` passed to `skill publish-submit --path` or `skill version-submit --path` is expanded locally before filtering and primary-document hashing; request a `file` source plan for this input. Directory inputs keep ZIP assets as ordinary files. Imports support stored/DEFLATE ZIPs up to the package ceiling compressed and expanded, with at most 1024 entries. Unsafe paths, symbolic links, encrypted ZIPs, ZIP64, conflicting entries, and corrupt content are refused. Include `SKILL.md`, `README.md`, or one unambiguous root Markdown document.
63
+
64
+ New Skills publish a DAG-CBOR SkillRoot hash in one graph bundle. Existing Skill identities keep ZIP-hash updates. New Skills require this updated CLI; it verifies every object and decrypts protected files locally into an ordinary ZIP. Historical ZIP downloads and whole-package decryption remain supported. A pending publication reuses its uploaded bytes and saved root commitment on resume.
65
+
66
+ The [publication ADR](../../docs/adr/ADR-0064-20260908-skillroot-publication.md) specifies the pinned draft, bundle/profile format, compatibility trust boundary, and rollout order.
67
+
68
+ Finch-managed and Lit downloads use Oracle V2 device-sealed grants and a non-extractable local CryptoKey.
69
+ The updated gateway also supports historical Finch-managed and Lit ZIPs through this delivery. It refuses
70
+ old bare-key buyer requests; use the updated CLI. Both providers use the same sealed delivery. See
71
+ [ADR-0065](../../docs/adr/ADR-0065-20260908-skill-oracle-v2-delivery.md) for deployment requirements.
72
+
73
+ Publication preparation accepts an optional `summary` (up to 500 characters). It is committed in
74
+ SkillRoot exactly as supplied, matching Browser publication metadata; omission means an empty summary.
75
+ Equal public file bytes, paths, name, summary, version and history produce equal roots. Encrypted
76
+ publications use random keys/IVs and need not produce the same root. Updates send the proposed
77
+ manifest for server validation; URI intents are prepared after the new chain binding is confirmed.
78
+
79
+ Finch-managed publication key requests use a signed timestamp and a one-time nonce. Each request is
80
+ valid for five minutes; retrying after expiry or a lost response requires a fresh signature. The
81
+ gateway rejects older CLI publication requests without timestamps. Historical downloads are unchanged.
82
+
83
+ ### Local publication failures
84
+
85
+ Directory publication rejects symbolic links, including internal relative links and broken links.
86
+ It skips excluded paths (for example `.git`, `node_modules`, `.aws`, and credential files) before reading
87
+ or descending into them. `excluded` lists a skipped directory once instead of enumerating its children.
88
+ Keep required resource paths functional when preparing a link-free source; deleting required links is
89
+ not a valid repair. ZIP imports also reject symbolic links.
90
+
91
+ Source errors keep exit code 2 and `AGENT_MARKET_REQUEST_INVALID`. Parse the JSON string in `error.state`
92
+ for `diagnosticCode`: `SKILL_SOURCE_SYMLINK_UNSUPPORTED`, `SKILL_SOURCE_UNREADABLE`, or
93
+ `SKILL_UPLOAD_SIZE_INVALID`, `SKILL_SOURCE_ZIP_INVALID`, `SKILL_SOURCE_PRIMARY_DOCUMENT_INVALID`, or `SKILL_MANIFEST_SIZE_INVALID`. Source diagnostics include a root-relative `path` (`.` for the selected
94
+ root); size diagnostics include `actualBytes` and `maxBytes`. The internal packaging bound is the package ceiling and applies to the final
95
+ upload object, including SkillRoot metadata and encryption overhead. STORE ZIP construction does not
96
+ compress files. A size rejection occurs before that upload and before transaction submission.
97
+
98
+ Publish, version, and publication-recovery failures also include `phase`, `fingerprint`,
99
+ `transactionHashes` (keyed by `primary`, `uri`, or `lit`), `submissionStatus`, and `recoveryCommand` in
100
+ `error.state`. `packaging` includes local hashing/encryption; `transaction` covers signing, broadcast,
101
+ and receipt handling. A validated remote error keeps its code, message, trace and HTTP status
102
+ (`remoteStatus`), with its original state retained as `remoteState`; publication retry guidance takes
103
+ precedence over a remote service's generic retry flag. Private causes, RPC URLs, credentials and
104
+ journal contents are never printed.
105
+
106
+ Publication failures report `retryable` and `nextAction` according to the failure. A projection failure after confirmed transactions can be retryable; inspect `unconfirmedActions` before claiming the complete publication succeeded. This is not permission to replay a paid operation.
107
+ `not_attempted` means no send was attempted in this invocation and no hashes were found in the
108
+ validated journal; it is not proof about other invocations. `hash_known` includes known transaction
109
+ hashes. `uncertain` means sending or durable checkpointing failed. For creation/version/key stages, contact support before retrying or
110
+ recovering, even when a hash is shown. A non-null `recoveryCommand` is provided only after a journal
111
+ is validated or saved and no submission/checkpoint uncertainty was observed. Recheck CLI/MCP identity
112
+ and use that command rather than repeating publish-submit. Cleanup failures do not claim a journal
113
+ still exists. If source is unavailable or its excluded-path inventory changed across CLI versions,
114
+ the fingerprint-only recovery command can use the already stored publication bytes.
115
+
116
+ These fixes require CLI 0.3.3 or newer; npm 0.3.2 does not include them. Updating a Skill does not upgrade the installed CLI. Journal v6 saves a pending stage before signing/submitting. Missing hashes in creating stages of a pending or legacy v4/v5 journal block automatic resubmission; follow the reported chain-state checks and contact support. CLI 0.3.7 permits the URI assignment stage to converge through its typed recovery path. Known hashes are verified without rebroadcast. The pending marker also blocks destructive local authorization/wallet cleanup through the recovery inventory.
117
+
118
+ ## Bootstrap and health reports (CLI 0.3.4+)
119
+
120
+ First-wallet offline import, installed Account-switch revocation, partial doctor checks and exit code 2, contextual wallet recovery guidance, login authentication-chain help, and strict OAuth UUID response validation require CLI 0.3.4 or newer. Updating the website or Skill does not upgrade an installed CLI.
121
+
122
+ ## WebP image detection (CLI 0.3.5+)
123
+
124
+ CLI 0.3.5 fixes valid WebP covers and packaged detail images being rejected when binary file-size bytes were decoded as UTF-8. Upgrade the installed CLI to receive the local detection fix; website or Skill updates alone do not update the executable. Image types, size limits, and publication recovery rules are unchanged.
125
+
126
+ ## Market category compatibility (CLI 0.3.6+)
127
+
128
+ Use CLI 0.3.6 or newer for the expanded market categories. Skill response display categories now accept bounded text, so future taxonomy additions do not require another CLI upgrade. Updating the website or this Skill does not upgrade an installed CLI.
129
+
130
+ ## Staged uploads and external media (CLI 0.3.7+)
131
+
132
+ CLI 0.3.7 supports authorized external-media manifests and 30 MiB final packages on both upload and download. Browser publishing still uses multipart and refuses external media. Failed staged uploads attempt ticket release and report its result in the sanitized diagnostic. Presentation submission reports progress and whether setting the same URI can safely converge; a stale plan, authorization failure or invalid input still requires correction before retrying. Read the maintained Finch Market Skill for the complete external-host and recovery boundaries.
133
+
134
+ ## Diagnostic and interoperability fixes (CLI 0.3.9+)
135
+
136
+ CLI 0.3.9 removes the `mcp.supportedClients` field from doctor. Its health checks do not fetch client guides or migrate legacy wallet files. Omitted or empty OAuth scope uses the server-resolved registered defaults; explicit requested scopes remain checked. Callback target, original query values and state remain verified, independently of query ordering or authorization-code length. Upgrade the installed CLI to receive these fixes; updating the Skill or website alone does not apply them.
package/dist/finch.js CHANGED
@@ -17652,7 +17652,7 @@ var init_schemas = __esm({
17652
17652
  requiresHuman: { type: "boolean" },
17653
17653
  requiresTwitter: { type: "boolean" },
17654
17654
  autoSettle: { type: "boolean" },
17655
- settlementMode: { type: "string", enum: ["merchant_manual", "fcfs_realtime", "weighted_lottery", "delayed_auto_7d"] },
17655
+ settlementMode: { type: "string", enum: ["merchant_manual", "fcfs_realtime", "weighted_lottery", "delayed_auto_7d", "merchant_review_7d"] },
17656
17656
  deadline: nullableString,
17657
17657
  startAt: nullableString,
17658
17658
  goals: { type: "array", maxItems: 5, items: openObjectSchema },
@@ -17665,7 +17665,7 @@ var init_schemas = __esm({
17665
17665
  }, ["content"]);
17666
17666
  taskReviewRequestSchema = strictObjectSchema2({
17667
17667
  submissionIds: { type: "array", minItems: 1, maxItems: 100, uniqueItems: true, items: canonicalUuidSchema2 },
17668
- action: { type: "string", enum: ["approve", "cancel_award", "reject", "spam", "favorite", "unfavorite"] },
17668
+ action: { type: "string", enum: ["approve", "cancel_award", "reject", "review_later", "spam", "favorite", "unfavorite"] },
17669
17669
  spamReason: { type: "string", maxLength: 200 }
17670
17670
  }, ["submissionIds", "action"]);
17671
17671
  signedAwardSchema = strictObjectSchema2({
@@ -55201,6 +55201,18 @@ function jsonObject(value) {
55201
55201
  }
55202
55202
  return value;
55203
55203
  }
55204
+ function includesCapabilities(value, required4) {
55205
+ return Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0) && required4.every((item) => value.includes(item));
55206
+ }
55207
+ function sameOriginEndpoint(value, origin2) {
55208
+ if (typeof value !== "string") return false;
55209
+ try {
55210
+ const url2 = new URL(value);
55211
+ return url2.origin === origin2 && url2.username === "" && url2.password === "" && url2.hash === "";
55212
+ } catch {
55213
+ return false;
55214
+ }
55215
+ }
55204
55216
  function protectedResourceMetadataAuthority(authenticate, origin2, resourcePath) {
55205
55217
  const matches = [...authenticate.matchAll(/(?:^|[\s,])resource_metadata="([^"]+)"/gu)];
55206
55218
  if (!/(?:^|[\s,])Bearer(?:[\s,]|$)/iu.test(authenticate) || matches.length !== 1) {
@@ -55465,7 +55477,8 @@ var init_api2 = __esm({
55465
55477
  method: options.method.toUpperCase(),
55466
55478
  headers,
55467
55479
  body,
55468
- redirect: "error"
55480
+ redirect: "error",
55481
+ ...options.signal === void 0 ? {} : { signal: options.signal }
55469
55482
  });
55470
55483
  } catch (cause) {
55471
55484
  throw new CliHttpError("CLI_REMOTE_UNAVAILABLE", null, { cause });
@@ -55534,7 +55547,8 @@ var init_api2 = __esm({
55534
55547
  return await this.fetchImplementation(`${this.origin}${path2}`, {
55535
55548
  method: "GET",
55536
55549
  redirect: "error",
55537
- cache: "no-store"
55550
+ cache: "no-store",
55551
+ signal: AbortSignal.timeout(15e3)
55538
55552
  });
55539
55553
  } catch (cause) {
55540
55554
  throw new CliHttpError("CLI_REMOTE_UNAVAILABLE", null, { cause });
@@ -55563,7 +55577,7 @@ var init_api2 = __esm({
55563
55577
  throw new CliHttpError("CLI_REMOTE_UNAVAILABLE", resourceResponse.status);
55564
55578
  }
55565
55579
  const resource = jsonObject(await readBoundedJson(resourceResponse));
55566
- if (resource.resource !== resourceMetadataAuthority.resource || JSON.stringify(resource.authorization_servers) !== JSON.stringify([this.origin])) {
55580
+ if (resource.resource !== resourceMetadataAuthority.resource || !includesCapabilities(resource.authorization_servers, [this.origin])) {
55567
55581
  throw new CliHttpError("CLI_RESPONSE_INVALID", resourceResponse.status);
55568
55582
  }
55569
55583
  const authorizationResponse = await this.publicFetch("/.well-known/oauth-authorization-server");
@@ -55572,33 +55586,14 @@ var init_api2 = __esm({
55572
55586
  throw new CliHttpError("CLI_REMOTE_UNAVAILABLE", authorizationResponse.status);
55573
55587
  }
55574
55588
  const authorization = jsonObject(await readBoundedJson(authorizationResponse));
55575
- if (authorization.issuer !== this.origin || authorization.authorization_endpoint !== `${this.origin}/oauth/authorize` || authorization.token_endpoint !== `${this.origin}/oauth/token` || authorization.registration_endpoint !== `${this.origin}/oauth/register` || JSON.stringify(authorization.response_types_supported) !== JSON.stringify(["code"]) || JSON.stringify(authorization.grant_types_supported) !== JSON.stringify(["authorization_code", "refresh_token"]) || JSON.stringify(authorization.token_endpoint_auth_methods_supported) !== JSON.stringify(["none"]) || JSON.stringify(authorization.code_challenge_methods_supported) !== JSON.stringify(["S256"])) {
55589
+ if (authorization.issuer !== this.origin || authorization.authorization_endpoint !== `${this.origin}/oauth/authorize` || !sameOriginEndpoint(authorization.token_endpoint, this.origin) || !sameOriginEndpoint(authorization.registration_endpoint, this.origin) || !includesCapabilities(authorization.response_types_supported, ["code"]) || !includesCapabilities(authorization.grant_types_supported, ["authorization_code", "refresh_token"]) || !includesCapabilities(authorization.token_endpoint_auth_methods_supported, ["none"]) || !includesCapabilities(authorization.code_challenge_methods_supported, ["S256"])) {
55576
55590
  throw new CliHttpError("CLI_RESPONSE_INVALID", authorizationResponse.status);
55577
55591
  }
55578
- const manifestResponse = await this.publicFetch("/mcp/client-compatibility.json");
55579
- if (manifestResponse.status !== 200) {
55580
- await manifestResponse.body?.cancel().catch(() => void 0);
55581
- throw new CliHttpError("CLI_REMOTE_UNAVAILABLE", manifestResponse.status);
55582
- }
55583
- const manifest4 = jsonObject(await readBoundedJson(manifestResponse));
55584
- const mcp = jsonObject(manifest4.mcp);
55585
- const source = jsonObject(manifest4.source);
55586
- if (!Array.isArray(manifest4.clients) || manifest4.clients.length !== 1) {
55587
- throw new CliHttpError("CLI_RESPONSE_INVALID", manifestResponse.status);
55588
- }
55589
- const client = jsonObject(manifest4.clients[0]);
55590
- const install = jsonObject(client.install);
55591
- const authorize = jsonObject(client.authorize);
55592
- const verify = jsonObject(client.verify);
55593
- if (manifest4.schemaVersion !== 3 || manifest4.manifestId !== "finch.remote-mcp-clients" || mcp.serverName !== "finch" || mcp.url !== mcpUrl || mcp.transport !== "streamable_http" || mcp.authorization !== "oauth" || source.repository !== "https://github.com/FinchipAI/Finch-Site" || source.path !== "apps/mcp/src/client-compatibility.ts" || client.id !== "codex" || client.supportLevel !== "verified" || install.kind !== "native_cli" || install.executable !== "codex" || JSON.stringify(install.arguments) !== JSON.stringify(["mcp", "add", "finch", "--url", mcpUrl]) || authorize.kind !== "native_cli" || authorize.executable !== "codex" || JSON.stringify(authorize.arguments) !== JSON.stringify(["mcp", "login", "finch"]) || verify.kind !== "harness_ui" || verify.surface !== "mcp_tools" || client.reload !== "new_session") {
55594
- throw new CliHttpError("CLI_RESPONSE_INVALID", manifestResponse.status);
55595
- }
55596
55592
  return Object.freeze({
55597
55593
  url: mcpUrl,
55598
55594
  transport: "streamable_http",
55599
55595
  authorization: "oauth",
55600
- compatibilityManifestUrl,
55601
- supportedClients: Object.freeze(["codex"])
55596
+ compatibilityManifestUrl
55602
55597
  });
55603
55598
  }
55604
55599
  async uploadSkillObject(input) {
@@ -55900,7 +55895,7 @@ var init_api2 = __esm({
55900
55895
  if (response.status === 202 && body?.status === "indexing_pending") return "indexing_pending";
55901
55896
  throw new CliHttpError("CLI_RESPONSE_INVALID", response.status);
55902
55897
  }
55903
- getSession(bearer) {
55898
+ getSession(bearer, signal) {
55904
55899
  const operation3 = IDENTITY_V1_OPERATIONS.session;
55905
55900
  return this.request({
55906
55901
  method: operation3.method,
@@ -55908,7 +55903,8 @@ var init_api2 = __esm({
55908
55903
  successStatus: operation3.successStatus,
55909
55904
  errorContract: "identity",
55910
55905
  responseSchema: operation3.responseSchema,
55911
- bearer
55906
+ bearer,
55907
+ ...signal === void 0 ? {} : { signal }
55912
55908
  });
55913
55909
  }
55914
55910
  authorizeMcp(requestPath, bearer) {
@@ -57204,7 +57200,7 @@ var init_application = __esm({
57204
57200
  const redirectValues = request.searchParams.getAll("redirect_uri");
57205
57201
  const stateValues = request.searchParams.getAll("state");
57206
57202
  const scopeValues = request.searchParams.getAll("scope");
57207
- if (request.origin !== profile.origin || request.pathname !== "/oauth/authorize" || request.username !== "" || request.password !== "" || request.hash !== "" || requestUrl !== `${profile.origin}${request.pathname}${request.search}` || redirectValues.length !== 1 || stateValues.length > 1 || scopeValues.length !== 1) {
57203
+ if (request.origin !== profile.origin || request.pathname !== "/oauth/authorize" || request.username !== "" || request.password !== "" || request.hash !== "" || requestUrl !== `${profile.origin}${request.pathname}${request.search}` || redirectValues.length !== 1 || stateValues.length > 1 || scopeValues.length > 1) {
57208
57204
  throw new TypeError("MCP OAuth request URL does not match the active profile authority.");
57209
57205
  }
57210
57206
  let registeredRedirect;
@@ -57221,10 +57217,10 @@ var init_application = __esm({
57221
57217
  if (response.authorized !== true || response.accountId !== authority.session.accountId || String(response.walletAddress).toLowerCase() !== authority.walletAddress || typeof response.clientName !== "string" || response.clientName.length < 1 || response.clientName.length > 128 || !Array.isArray(response.scopes) || response.scopes.some((scope) => typeof scope !== "string" || scope.length < 1) || typeof response.callbackUrl !== "string") {
57222
57218
  throw new CliAuthorityError();
57223
57219
  }
57224
- const requestedScopes = scopeValues[0].split(" ").filter(Boolean).sort();
57225
- const returnedScopes = [...response.scopes].sort();
57226
- if (requestedScopes.length < 1 || requestedScopes.length !== returnedScopes.length || requestedScopes.some((scope, index2) => scope !== returnedScopes[index2])) {
57227
- throw new CliAuthorityError();
57220
+ const requestedScopes = [...new Set((scopeValues[0] ?? "").split(" ").filter(Boolean))].sort();
57221
+ const returnedScopes = [...new Set(response.scopes)].sort();
57222
+ if (returnedScopes.length < 1 || requestedScopes.length > 0 && (requestedScopes.length !== returnedScopes.length || requestedScopes.some((scope, index2) => scope !== returnedScopes[index2]))) {
57223
+ throw new TypeError("MCP OAuth granted scopes do not match the authorization request.");
57228
57224
  }
57229
57225
  let callback;
57230
57226
  try {
@@ -57233,13 +57229,17 @@ var init_application = __esm({
57233
57229
  throw new TypeError("MCP OAuth callback is invalid.");
57234
57230
  }
57235
57231
  const codes = callback.searchParams.getAll("code");
57236
- if (codes.length !== 1 || !/^[A-Za-z0-9_-]{43}$/u.test(codes[0])) {
57232
+ if (codes.length !== 1 || codes[0].length < 1 || callback.searchParams.has("error")) {
57237
57233
  throw new TypeError("MCP OAuth callback is invalid.");
57238
57234
  }
57239
57235
  const expectedCallback = new URL(registeredRedirect.href);
57240
57236
  expectedCallback.searchParams.set("code", codes[0]);
57241
57237
  if (stateValues.length === 1) expectedCallback.searchParams.set("state", stateValues[0]);
57242
- if (callback.href !== expectedCallback.href) {
57238
+ const expectedParameters = new URLSearchParams(expectedCallback.search);
57239
+ const callbackParameters = new URLSearchParams(callback.search);
57240
+ expectedParameters.sort();
57241
+ callbackParameters.sort();
57242
+ if (callback.origin !== registeredRedirect.origin || callback.pathname !== registeredRedirect.pathname || callback.protocol !== registeredRedirect.protocol || callback.username !== "" || callback.password !== "" || callback.hash !== "" || callbackParameters.toString() !== expectedParameters.toString()) {
57243
57243
  throw new TypeError("MCP OAuth callback does not match the registered redirect and state.");
57244
57244
  }
57245
57245
  await required2(this.browserLauncher, "browser launcher").open(callback.href);
@@ -57412,7 +57412,8 @@ var init_application = __esm({
57412
57412
  message: localFailureMessage(cause)
57413
57413
  }];
57414
57414
  } else if (cause instanceof CliHttpError) {
57415
- return [null, { status: cause.status === 401 || cause.status === 403 || cause.code === "CLI_RESPONSE_INVALID" ? "invalid" : "unavailable", message: guidance }];
57415
+ const unavailable = cause.status === 429 || cause.status !== null && cause.status >= 500;
57416
+ return [null, { status: !unavailable && (cause.status === 401 || cause.status === 403 || cause.code === "CLI_RESPONSE_INVALID") ? "invalid" : "unavailable", message: guidance }];
57416
57417
  } else if (cause instanceof TypeError || cause instanceof CliAuthorityError) {
57417
57418
  return [null, { status: "invalid", message: guidance }];
57418
57419
  } else {
@@ -57425,7 +57426,7 @@ var init_application = __esm({
57425
57426
  "Check the protected FINCHTECH_HOME/profile.json endpoint override."
57426
57427
  );
57427
57428
  const [wallet, walletCheck] = await inspect(
57428
- () => required2(this.state.readWalletAddress, "state.readWalletAddress").call(this.state),
57429
+ () => required2(this.state.readWalletAddress, "state.readWalletAddress").call(this.state, { migrateLegacy: false }),
57429
57430
  "Restore the current wallet from backup."
57430
57431
  );
57431
57432
  const [session, sessionCheck] = await inspect(
@@ -57439,7 +57440,7 @@ var init_application = __esm({
57439
57440
  if (session.origin !== profile.origin || wallet.walletAddress !== session.walletAddress) {
57440
57441
  throw new CliAuthorityError();
57441
57442
  }
57442
- const remote = asObject(await required2(this.api.getSession, "api.getSession").call(this.api, session.rawBearer), "MCP doctor Session");
57443
+ const remote = asObject(await required2(this.api.getSession, "api.getSession").call(this.api, session.rawBearer, AbortSignal.timeout(15e3)), "MCP doctor Session");
57443
57444
  if (remote.sessionId !== session.sessionId || remote.accountId !== session.accountId || remote.walletAddress !== session.walletAddress || remote.environment !== session.environment || remote.audience !== "agent_cli" || typeof remote.sessionExpiresAt !== "string" || !Number.isFinite(Date.parse(remote.sessionExpiresAt)) || Date.parse(remote.sessionExpiresAt) <= Date.now()) throw new CliAuthorityError();
57444
57445
  return { sessionExpiresAt: remote.sessionExpiresAt };
57445
57446
  }, "Verify the endpoint and current wallet, then run finch login --help to renew the CLI login. If the server is unavailable, retry later.");
@@ -57451,7 +57452,7 @@ var init_application = __esm({
57451
57452
  if (profile !== null) {
57452
57453
  [mcp, mcpDiscoveryCheck] = await inspect(async () => {
57453
57454
  const discovery = await required2(this.api.inspectRemoteMcp, "api.inspectRemoteMcp").call(this.api);
57454
- if (discovery.url !== `${profile.origin}/mcp` || discovery.authorization !== "oauth" || JSON.stringify(discovery.supportedClients) !== JSON.stringify(["codex"])) {
57455
+ if (discovery.url !== `${profile.origin}/mcp` || discovery.authorization !== "oauth" || discovery.transport !== "streamable_http") {
57455
57456
  throw new TypeError("MCP discovery authority mismatch.");
57456
57457
  }
57457
57458
  return discovery;
@@ -58304,7 +58305,7 @@ var init_package = __esm({
58304
58305
  "package.json"() {
58305
58306
  package_default = {
58306
58307
  name: "@finchtech/cli",
58307
- version: "0.3.8",
58308
+ version: "0.3.9",
58308
58309
  description: "Finch Agent CLI for wallet-owned marketplace and MCP workflows.",
58309
58310
  type: "module",
58310
58311
  bin: {
@@ -59133,7 +59134,13 @@ function privateKey(value) {
59133
59134
  function validatedWallet(input) {
59134
59135
  const address2 = walletAddress(input.walletAddress);
59135
59136
  const key = privateKey(input.privateKey);
59136
- if (privateKeyToAccount(key).address.toLowerCase() !== address2) {
59137
+ let derivedAddress;
59138
+ try {
59139
+ derivedAddress = privateKeyToAccount(key).address.toLowerCase();
59140
+ } catch (cause) {
59141
+ throw new CliStateError("CLI_STATE_CORRUPT", { cause });
59142
+ }
59143
+ if (derivedAddress !== address2) {
59137
59144
  throw new CliStateError("CLI_STATE_CORRUPT");
59138
59145
  }
59139
59146
  return Object.freeze({ walletAddress: address2, privateKey: key });
@@ -59205,13 +59212,7 @@ var init_wallet_store = __esm({
59205
59212
  await this.writeIndex(nextIndex);
59206
59213
  if (makeCurrent || currentIndex.length === 0) await this.writeCurrent(wallet.walletAddress);
59207
59214
  }
59208
- async migrateLegacyOnce() {
59209
- try {
59210
- await this.files.readJson("wallet/current.json");
59211
- return;
59212
- } catch (cause) {
59213
- if (!(cause instanceof CliStateError) || cause.code !== "CLI_STATE_NOT_FOUND") throw cause;
59214
- }
59215
+ async readLegacy() {
59215
59216
  const readOptional = async (read2) => {
59216
59217
  try {
59217
59218
  return await read2();
@@ -59226,15 +59227,26 @@ var init_wallet_store = __esm({
59226
59227
  ]);
59227
59228
  if (metadata === null && key === null) {
59228
59229
  const index2 = await this.readIndex();
59229
- if (index2.length === 0) return;
59230
+ if (index2.length === 0) return null;
59230
59231
  throw new CliStateError("CLI_STATE_CORRUPT");
59231
59232
  }
59232
59233
  if (metadata === null || key === null) throw new CliStateError("CLI_STATE_CORRUPT");
59233
59234
  const legacy = record4(metadata);
59234
- await this.registerInternal(validatedWallet({
59235
+ return validatedWallet({
59235
59236
  walletAddress: legacy.walletAddress,
59236
59237
  privateKey: key.toString("utf8")
59237
- }), true);
59238
+ });
59239
+ }
59240
+ async migrateLegacyOnce() {
59241
+ try {
59242
+ await this.files.readJson("wallet/current.json");
59243
+ return;
59244
+ } catch (cause) {
59245
+ if (!(cause instanceof CliStateError) || cause.code !== "CLI_STATE_NOT_FOUND") throw cause;
59246
+ }
59247
+ const legacy = await this.readLegacy();
59248
+ if (legacy === null) return;
59249
+ await this.registerInternal(legacy, true);
59238
59250
  await this.files.remove("wallet/private-key");
59239
59251
  await this.files.remove("wallet/metadata.json");
59240
59252
  }
@@ -59267,9 +59279,17 @@ var init_wallet_store = __esm({
59267
59279
  await this.migrateLegacy();
59268
59280
  return this.readIndex();
59269
59281
  }
59270
- async readCurrent() {
59271
- await this.migrateLegacy();
59272
- const current = record4(await this.files.readJson("wallet/current.json"));
59282
+ async readCurrent(options = {}) {
59283
+ if (options.migrateLegacy !== false) await this.migrateLegacy();
59284
+ let current;
59285
+ try {
59286
+ current = record4(await this.files.readJson("wallet/current.json"));
59287
+ } catch (cause) {
59288
+ if (options.migrateLegacy !== false || !(cause instanceof CliStateError) || cause.code !== "CLI_STATE_NOT_FOUND") throw cause;
59289
+ const legacy = await this.readLegacy();
59290
+ if (legacy === null) throw cause;
59291
+ return legacy;
59292
+ }
59273
59293
  if (current.version !== 1) throw new CliStateError("CLI_STATE_CORRUPT");
59274
59294
  try {
59275
59295
  return await this.readRegistered(walletAddress(current.walletAddress));
@@ -59563,16 +59583,16 @@ var init_local_state = __esm({
59563
59583
  async saveWallet(input) {
59564
59584
  await this.wallets.register(input);
59565
59585
  }
59566
- async readWallet() {
59586
+ async readWallet(options) {
59567
59587
  try {
59568
- return await this.wallets.readCurrent();
59588
+ return await this.wallets.readCurrent(options);
59569
59589
  } catch (cause) {
59570
59590
  if (cause instanceof CliStateError) throw new CliStateError(cause.code, { cause, diagnostic: "wallet" });
59571
59591
  throw cause;
59572
59592
  }
59573
59593
  }
59574
- async readWalletAddress() {
59575
- const wallet = await this.readWallet();
59594
+ async readWalletAddress(options) {
59595
+ const wallet = await this.readWallet(options);
59576
59596
  return Object.freeze({ walletAddress: wallet.walletAddress });
59577
59597
  }
59578
59598
  async assertWalletBootstrap() {
@@ -60967,7 +60987,7 @@ var init_task_protocol_sender = __esm({
60967
60987
  const fields = record5(typedData.types, "Task award types").AwardAuthorization;
60968
60988
  const selected = taskDeployment(domain2.chainId);
60969
60989
  deployment ??= selected;
60970
- if (deployment.chainId !== selected.chainId || typedData.primaryType !== "AwardAuthorization" || domain2.name !== "Finch Task Vault" || domain2.version !== "0" || domain2.chainId !== deployment.chainId || JSON.stringify(fields) !== JSON.stringify(awardTypes.AwardAuthorization) || message.awardId !== record5(award.authorization, "Task award authorization").awardId) {
60990
+ if (deployment.chainId !== selected.chainId || typedData.primaryType !== "AwardAuthorization" || domain2.name !== "Finch Task Vault" || domain2.version !== "0" || domain2.chainId !== deployment.chainId || !Array.isArray(fields) || fields.length !== awardTypes.AwardAuthorization.length || fields.some((field, index2) => typeof field !== "object" || field === null || Array.isArray(field) || Object.keys(field).length !== 2 || field.name !== awardTypes.AwardAuthorization[index2].name || field.type !== awardTypes.AwardAuthorization[index2].type) || message.awardId !== record5(award.authorization, "Task award authorization").awardId) {
60971
60991
  throw new TypeError("Task award signing authority is invalid.");
60972
60992
  }
60973
60993
  const canonicalDomain = {
@@ -61574,9 +61594,9 @@ function assetPlan(value, walletAddress2) {
61574
61594
  ], "Skill asset plan");
61575
61595
  const chainId = assertAuthority(plan, "asset_upload", walletAddress2, false);
61576
61596
  const skillId = string4(plan.skillId, "Skill id", /^[1-9][0-9]*$/u);
61577
- if (plan.assetKind !== "cover" || plan.maxBytes !== 5242880) throw new TypeError("Skill cover authority is invalid.");
61597
+ if (plan.assetKind !== "cover" || typeof plan.maxBytes !== "number" || !Number.isSafeInteger(plan.maxBytes) || plan.maxBytes < 1) throw new TypeError("Skill cover authority is invalid.");
61578
61598
  const accepted = plan.acceptedMediaTypes;
61579
- if (!Array.isArray(accepted) || JSON.stringify(accepted) !== JSON.stringify(["image/png", "image/jpeg", "image/webp", "image/gif"])) {
61599
+ if (!Array.isArray(accepted) || accepted.length < 1 || new Set(accepted).size !== accepted.length || accepted.some((mediaType) => typeof mediaType !== "string" || !/^image\/[a-z0-9.+-]+$/u.test(mediaType))) {
61580
61600
  throw new TypeError("Skill cover media authority is invalid.");
61581
61601
  }
61582
61602
  const action = object5(plan.localAction, "Skill local action");
@@ -62513,6 +62533,7 @@ var init_skill_local_flow = __esm({
62513
62533
  if (!stat.isFile() || stat.isSymbolicLink()) throw new TypeError("Skill cover path must be one regular file.");
62514
62534
  if (stat.size < 1 || stat.size > plan.maxBytes) throw new TypeError("Skill cover size is outside the prepared bound.");
62515
62535
  const bytes = new Uint8Array(await readFile2(absolute));
62536
+ if (bytes.byteLength < 1 || bytes.byteLength > plan.maxBytes) throw new TypeError("Skill cover size is outside the prepared bound.");
62516
62537
  const mediaType = detectedImageMediaType(bytes);
62517
62538
  if (mediaType === null || !plan.acceptedMediaTypes.includes(mediaType)) {
62518
62539
  throw new TypeError("Skill cover bytes do not match an accepted image type.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finchtech/cli",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Finch Agent CLI for wallet-owned marketplace and MCP workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,11 +34,11 @@
34
34
  "typescript-eslint": "8.67.0",
35
35
  "viem": "2.55.15",
36
36
  "vitest": "4.1.10",
37
- "@finch/skill-market-contracts": "0.1.0",
38
37
  "@finch/agent-market-contracts": "0.1.0",
39
- "@finch/skill-content": "0.1.0",
40
38
  "@finch/deployment-manifest": "0.1.0",
41
- "@finch/task-market-contracts": "0.1.0"
39
+ "@finch/skill-market-contracts": "0.1.0",
40
+ "@finch/task-market-contracts": "0.1.0",
41
+ "@finch/skill-content": "0.1.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "esbuild src/bootstrap.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/finch.js --banner:js=\"#!/usr/bin/env node\"",