@paidfor/mcp 0.5.0 → 0.7.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.
- package/README.md +49 -7
- package/dist/clients.d.ts +114 -0
- package/dist/clients.js +330 -0
- package/dist/init.d.ts +61 -0
- package/dist/init.js +629 -0
- package/dist/link.d.ts +45 -0
- package/dist/link.js +109 -0
- package/dist/pay.d.ts +16 -1
- package/dist/pay.js +58 -7
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +66 -0
- package/dist/rules.js +1 -1
- package/dist/server.d.ts +3 -8
- package/dist/server.js +4 -13
- package/dist/setup.d.ts +3 -1
- package/dist/setup.js +5 -5
- package/dist/stdio.js +18 -2
- package/dist/team.d.ts +80 -0
- package/dist/team.js +170 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.js +22 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
MCP server for [paidfor.dev](https://paidfor.dev), the x402 seller failure index on Base mainnet.
|
|
6
6
|
|
|
7
|
-
**No account, no key.** Everything that reads the index or contributes data (the site, the API, the MCP tools, the client, telemetry and the changes feed) needs no account and no key, now and later. An email address is asked for only to deliver alerts or to join a waitlist, as a confirmation link, never a profile.
|
|
8
|
-
|
|
9
7
|
[](https://cursor.com/install-mcp?name=paidfor&config=eyJ1cmwiOiJodHRwczovL21jcC5wYWlkZm9yLmRldi9tY3AifQ%3D%3D)
|
|
10
8
|
|
|
11
9
|
Three read tools and one report tool on every transport, plus two paying tools on stdio only when a wallet is configured:
|
|
@@ -21,6 +19,36 @@ On initialize the server sends `instructions` telling the client to call `check_
|
|
|
21
19
|
|
|
22
20
|
The read tools use the public JSON API (`GET https://paidfor.dev/v1/check`, `/v1/sellers`, `/v1/changes`). No keys, no rate limit for now.
|
|
23
21
|
|
|
22
|
+
## Install in one command (0.7.0)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx -y @paidfor/mcp init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It finds the MCP clients on this machine (Claude Code, Codex CLI, Cursor, Claude Desktop, Windsurf / Devin Desktop, VS Code) and asks which to set up; asks whether this agent may pay x402 sellers and, only on a yes, makes a wallet in `~/.paidfor/wallet.key` (mode 600) with caps of $0.50 a call and $5 a day, or uses the one already there; offers to link that wallet to a paidfor account, which you approve in the browser after checking the code it prints matches; then shows each config change (key names, never values) and writes it after you say yes. Each client gets a stdio server named `paidfor` running `npx -y @paidfor/mcp@<this version>` (pinned), with `PAIDFOR_WALLET_KEY_FILE`, the two caps and `PAIDFOR_TEAM_KEY` in its env only when they apply. Last it checks `api-xpay.com` against the index (expected: `avoid`) and says what is left: which clients to restart, the address to fund, and whether the wallet is live or paused on the account.
|
|
29
|
+
|
|
30
|
+
Config files are merged, never replaced: other servers stay, an existing `paidfor` entry (such as the hosted URL, which cannot pay) is replaced but the caps you set in its env are kept, the original is copied once to `<file>.paidfor-bak`, the write is atomic and keeps the file's mode, and a file that is not valid JSON is left alone with a message. Claude Code, Codex and VS Code are set up through their own CLIs (`claude mcp add --scope user`, `codex mcp add`, `code --add-mcp`), which take the env on the command line; that command is never printed.
|
|
31
|
+
|
|
32
|
+
| flag | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `--yes` | no questions: every client found (or those in `--client`), no wallet unless `--wallet`, no link unless `--link`. Also what happens without a terminal |
|
|
35
|
+
| `--client <id>[,<id>]` | only these: `claude-code`, `codex`, `cursor`, `claude-desktop`, `windsurf`, `vscode` |
|
|
36
|
+
| `--wallet` / `--no-wallet` | let the agent pay, or not; an agent running init never makes a wallet unless told to |
|
|
37
|
+
| `--link` / `--no-link` | link the wallet to a paidfor account, or not |
|
|
38
|
+
| `--wait` | with `--yes` (or without a terminal), wait for the approval in the browser instead of saving the link for `link` to finish |
|
|
39
|
+
| `--key-file <path>` | use this wallet key instead of `~/.paidfor/wallet.key` (implies `--wallet`) |
|
|
40
|
+
| `--label <name>` | the wallet's name on the account (default: this machine's host name) |
|
|
41
|
+
| `--dry-run` | print what would change; writes nothing, makes no wallet, starts no link |
|
|
42
|
+
|
|
43
|
+
`npx -y @paidfor/mcp link` is the link, register and config steps alone, for a wallet set up without an account: it takes `--label`, `--key-file`, `--client`, `--wait`, `--yes` and `--dry-run`. A link that is denied, expires or was already used leaves everything else in place and says so; run `link` again later. The output is safe for an agent to read and relay: it never contains the wallet key, the team key or the link's device code.
|
|
44
|
+
|
|
45
|
+
**When an agent runs it.** An agent's shell shows output only when the command exits, so with `--yes` (or without a terminal) the link step does not wait for the approval. It is two steps:
|
|
46
|
+
|
|
47
|
+
1. `npx -y @paidfor/mcp init --yes --wallet --link` prints the approval link and the code, saves the pending link to `~/.paidfor/link-pending.json` (mode 600; the only place its device code is written), writes the config without a team key, and exits. Its next steps start with `1. Open the link above and approve it. 2. Then run: npx -y @paidfor/mcp@<version> link`. The agent hands the link and the code to its user.
|
|
48
|
+
2. After the user approves, `npx -y @paidfor/mcp link --yes` resumes the saved link: it checks at once and for up to 20 seconds, then registers the wallet, writes the team key into the clients the first run configured (or `--client`), says whether the wallet is live or paused, and deletes the record. Still not approved: it says so, keeps the record and exits 0, so it can be run again. Denied, expired or already used: it says so and deletes the record.
|
|
49
|
+
|
|
50
|
+
A saved link is resumed only by a run against the same `PAIDFOR_API_BASE` and wallet, before it expires; otherwise `link` starts a new one. If the browser approval went through but registering the wallet failed, the approved team key is kept in that same file (never in a client config) and the next `link` (or `init --link`) registers again with it, no new approval needed; the record is deleted once it succeeds, or dropped if paidfor answers 401 for the key (revoked). In a terminal, `init` and `link` wait for the approval as before.
|
|
51
|
+
|
|
24
52
|
## Hosted (Streamable HTTP)
|
|
25
53
|
|
|
26
54
|
`https://mcp.paidfor.dev/mcp`; a plain `GET` on it (or on `/status`) returns the service status with the data `generated_at` it last saw. A browser (`Accept` preferring `text/html`) gets a setup page from `src/setup.ts` on `/mcp` and `/` instead; `?format=json` brings the status back. `/health` on that host is answered by Manufact's gateway. Cursor, Claude Code and any Streamable HTTP client take the URL directly; Claude Desktop takes it under Settings, Connectors, or through `mcp-remote`:
|
|
@@ -45,9 +73,11 @@ The read tools use the public JSON API (`GET https://paidfor.dev/v1/check`, `/v1
|
|
|
45
73
|
|
|
46
74
|
## Let your agent pay, safely (stdio only)
|
|
47
75
|
|
|
48
|
-
`pay_x402` pays an x402 seller with **your own wallet**, through `@paidfor/x402-safe-fetch` 0.
|
|
76
|
+
`pay_x402` pays an x402 seller with **your own wallet**, through `@paidfor/x402-safe-fetch` 0.4.0: it checks the seller on paidfor first and refuses `avoid` sellers, refuses quotes over 1.1x the listed price, caps the amount per call and per day, never pays twice for the same `intent_id` (a retry returns the stored result), keeps receipts in `~/.paidfor/receipts/`, and reports the outcome with its proof of payment (payer, nonce, pay-to, tx hash) to `https://paidfor.dev/v1/telemetry`, where it is checked on Base. It returns the status, the body (truncated at 64 KB) and a receipt with the amount, pay-to, tx hash and basescan link, the seller verdict at purchase, the validator results and the `verification_url` that shows whether the index verified it.
|
|
77
|
+
|
|
78
|
+
**Use a dedicated wallet holding a few dollars, never your main wallet.** There is no custody: the key stays on your machine, is read only to sign, and is never logged, returned by a tool or sent anywhere.
|
|
49
79
|
|
|
50
|
-
|
|
80
|
+
`npx -y @paidfor/mcp init` does all of the below for you. By hand:
|
|
51
81
|
|
|
52
82
|
```bash
|
|
53
83
|
npx -y @paidfor/mcp wallet new
|
|
@@ -76,7 +106,15 @@ Verify by asking the agent to call `wallet_status`.
|
|
|
76
106
|
PAIDFOR_TEAM_KEY=pft_... npx -y @paidfor/mcp wallet register --label "research agent"
|
|
77
107
|
```
|
|
78
108
|
|
|
79
|
-
It asks paidfor for a challenge, signs it with the wallet's key (the key is sent nowhere) and sends the signature back. The wallet is `PAIDFOR_WALLET_KEY_FILE` or `PAIDFOR_WALLET_KEY` when set, else `~/.paidfor/wallet.key
|
|
109
|
+
It asks paidfor for a challenge, signs it with the wallet's key (the key is sent nowhere) and sends the signature back. The wallet is `PAIDFOR_WALLET_KEY_FILE` or `PAIDFOR_WALLET_KEY` when set, else `~/.paidfor/wallet.key`; if the MCP client is configured with a key file elsewhere, set `PAIDFOR_WALLET_KEY_FILE` to that same path for this command too, or it registers the default wallet instead. A wallet registered with a key starts paused: open the team page, check its limits, untick Paused and save. The team key is read from the environment only, never from an argument. Written into the command it stays in your shell history; to keep it out, `read -rs PAIDFOR_TEAM_KEY && export PAIDFOR_TEAM_KEY` first and run the command without the prefix. Registering does not configure the server: `pay_x402` still needs `PAIDFOR_WALLET_KEY_FILE` in the MCP client config, as above.
|
|
110
|
+
|
|
111
|
+
**Ask the team before each payment (0.6.0).** Registering alone does not put payments under the team's rules. Give the server the team key too:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
"env": { "PAIDFOR_WALLET_KEY_FILE": "/Users/you/.paidfor/wallet.key", "PAIDFOR_TEAM_KEY": "pft_..." }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
With `PAIDFOR_TEAM_KEY` set, `pay_x402` asks `POST /v1/team/authorize` after its own checks and before it signs, and the local caps still apply. It pays only on an approval, and signs with the approval's reference as the EIP-3009 nonce and its deadline as `validBefore`, so the payment can be matched to the ledger. If the server is unreachable or answers anything but a clear decision, nothing is paid. A decline comes back with the team's reason. A payment waiting for a person comes back with the link to approve it and the `intent_id` to call again with. After a payment the outcome goes to `POST /v1/team/outcome`; a report the server cannot take is kept in `~/.paidfor/team-outcomes/` and sent before the next payment. The key sits in your MCP client's config, so treat that file as a secret. The key cannot change rules, keys or approvals. The agent holds its own wallet key here, so a payment made some other way is not stopped. Stdio only: the hosted server never pays.
|
|
80
118
|
|
|
81
119
|
**Routing a paid URL (0.4.1).** The model should reach for `pay_x402` whenever a URL is paid, metered, pay-per-call, x402, priced in USDC, said to charge a fee, or has answered 402 to any other fetch, even when the client also has a generic web fetch tool and the user never says how to pay. The tool is described as the way to fetch such a URL end to end (it sends the request, handles the 402, pays under the caps and returns the body), the description says a plain fetch fails on these URLs with 402, and the stdio server adds the same rule to its `instructions` only when the paying tools are registered; the hosted server's text is unchanged. `eval/routing.mjs` measures the first-call rate per model and prompt shape (`pnpm eval:routing`; results by date in `eval/routing-<date>.md`). `PAIDFOR_FETCH_ALIAS=1` also registers the same handler as `fetch_paid_url`, for clients that route by a fetch-shaped name; the eval compares both and the alias is off by default.
|
|
82
120
|
|
|
@@ -87,6 +125,7 @@ It asks paidfor for a challenge, signs it with the wallet's key (the key is sent
|
|
|
87
125
|
| `PAIDFOR_MAX_PER_CALL_USD` | `0.50` | most one call may pay; `max_usd` can lower it, never raise it |
|
|
88
126
|
| `PAIDFOR_DAILY_CAP_USD` | `5` | most all calls may pay per UTC day (tracked in `~/.paidfor/spend.json`) |
|
|
89
127
|
| `BASE_RPC_URL` | `https://mainnet.base.org` | used only by `wallet_status` for the balance |
|
|
128
|
+
| `PAIDFOR_TEAM_KEY` | | with a wallet: ask the team's rules before each payment and report the outcome (`https` only, or a server on this machine); also used by `wallet register` |
|
|
90
129
|
| `PAIDFOR_FETCH_ALIAS` | off | `1` also registers `pay_x402` as `fetch_paid_url` (same handler, schema and caps) |
|
|
91
130
|
|
|
92
131
|
Without a key (or with a misconfigured one, reported on stderr) the stdio server is the read-only server. The hosted server at `mcp.paidfor.dev` and the site's `/mcp` route never register `pay_x402` or `wallet_status`, by design: they never hold a key.
|
|
@@ -121,7 +160,7 @@ Usage is counted by Manufact's gateway (every `tools/call` with client and sessi
|
|
|
121
160
|
|
|
122
161
|
## Tests
|
|
123
162
|
|
|
124
|
-
`pnpm test` builds and runs `test/*.test.mjs` with Node's test runner: `hosted-snapshot` fails on any change to the hosted server's tool list, instructions or empty resources/prompts lists (the ChatGPT app review rescans them daily; regenerate on purpose with `UPDATE_SNAPSHOT=1`), `pay-wording` checks the `pay_x402` framing, guarantees, annotations and the alias, `pay-replay` the replay line and the `intent_id` guidance against a local seller. The site's `vitest` suite (`test/mcp-pay.test.ts` at the repo root) covers the paying tools end to end against a local seller.
|
|
163
|
+
`pnpm test` builds and runs `test/*.test.mjs` with Node's test runner: `hosted-snapshot` fails on any change to the hosted server's tool list, instructions or empty resources/prompts lists (the ChatGPT app review rescans them daily; regenerate on purpose with `UPDATE_SNAPSHOT=1`), `pay-wording` checks the `pay_x402` framing, guarantees, annotations and the alias, `pay-replay` the replay line and the `intent_id` guidance against a local seller, `pay-team` team mode against a local seller and a scripted server, `wallet-register` the challenge signing, `init` the one-command install against a fake home, fake client CLIs and a stand-in server. The site's `vitest` suite (`test/mcp-pay.test.ts` at the repo root) covers the paying tools end to end against a local seller, and `test/mcp-team.test.ts` runs team mode against the real server code.
|
|
125
164
|
|
|
126
165
|
## Claude Desktop extension
|
|
127
166
|
|
|
@@ -133,6 +172,9 @@ Usage is counted by Manufact's gateway (every `tools/call` with client and sessi
|
|
|
133
172
|
|
|
134
173
|
## Changelog
|
|
135
174
|
|
|
136
|
-
- **0.
|
|
175
|
+
- **0.7.0**: `paidfor-mcp init`, the install in one command (detects the clients, offers a wallet, links it to an account in the browser, writes the config, verifies), and `paidfor-mcp link`, which also finishes a link that `init --yes` saved instead of waiting for (`--wait` waits). `wallet register` takes the link's code so a wallet a person approved starts unpaused. `INIT_COMMAND` in `@paidfor/mcp/setup`. New test `init.test.mjs`. The hosted server's tools and instructions are unchanged.
|
|
176
|
+
|
|
177
|
+
- **0.6.0**: team mode. With `PAIDFOR_TEAM_KEY` set, `pay_x402` asks the team before signing, fails closed, signs the approval's nonce and deadline and reports the outcome. Needs `@paidfor/x402-safe-fetch` 0.4.0. The hosted server's tools and instructions are unchanged.
|
|
178
|
+
- **0.5.0**: `paidfor-mcp wallet register` registers the configured wallet to a paidfor team (`PAIDFOR_TEAM_KEY`), signing the team's challenge locally; new test `wallet-register.test.mjs`. The hosted server's tools and instructions are unchanged.
|
|
137
179
|
- **0.4.1** (unpublished): `pay_x402` is framed as the way to fetch a paid (x402) URL end to end, its description says a generic fetch fails on such URLs with 402 and that any URL called paid, metered, x402 or that answered 402 goes to it with the same method, headers and body; the stdio server with a wallet adds the same routing rule to its instructions (`PAY_ROUTING_RULE` in `rules.ts`). A replayed result (same `intent_id`) now opens with the age and time of the stored result and says to call again with a new id for a new purchase; the description, the schema and the stdio rules say to reuse an `intent_id` only when retrying the same purchase after an error or timeout and to omit it otherwise (a date-only id chosen in three fresh sessions replayed instead of buying). Optional `fetch_paid_url` alias (`PAIDFOR_FETCH_ALIAS=1`). New `pnpm test` (hosted-surface snapshot, wording) and `pnpm eval:routing`. The hosted server's tools and instructions are unchanged.
|
|
138
180
|
- **0.4.0**: `report_x402_payment` on every transport; `mcpName` `dev.paidfor/paidfor`.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Env } from "./wallet.js";
|
|
2
|
+
/** The clients init knows, in the order they are found and listed. */
|
|
3
|
+
export declare const CLIENT_IDS: readonly ["claude-code", "codex", "cursor", "claude-desktop", "windsurf", "vscode"];
|
|
4
|
+
/** One of CLIENT_IDS. */
|
|
5
|
+
export type ClientId = (typeof CLIENT_IDS)[number];
|
|
6
|
+
/** Whether a --client value names a known client. */
|
|
7
|
+
export declare const isClientId: (s: string) => s is ClientId;
|
|
8
|
+
/** The env keys whose values are secrets: never printed, masked in any command shown. */
|
|
9
|
+
export declare const SECRET_ENV: string[];
|
|
10
|
+
/** Values a person may have tuned in an existing entry; init keeps them. */
|
|
11
|
+
export declare const CAP_ENV: string[];
|
|
12
|
+
/** The stdio server entry init writes: command, args and env. */
|
|
13
|
+
export type ServerEntry = {
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
env: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
/** What a child process left: its exit code and output. */
|
|
19
|
+
export type ExecResult = {
|
|
20
|
+
code: number;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
};
|
|
24
|
+
/** Runs a command without a shell (injected in tests). */
|
|
25
|
+
export type Exec = (cmd: string, args: string[]) => Promise<ExecResult>;
|
|
26
|
+
/** Where to look for clients: the home, the platform, the env and PATH. */
|
|
27
|
+
export type ClientContext = {
|
|
28
|
+
home: string;
|
|
29
|
+
platform: NodeJS.Platform;
|
|
30
|
+
env: Env;
|
|
31
|
+
/** The absolute path of `bin` on PATH, or null. */
|
|
32
|
+
which: (bin: string) => Promise<string | null>;
|
|
33
|
+
};
|
|
34
|
+
/** What every target has: its id, its name and how to restart it. */
|
|
35
|
+
type Base = {
|
|
36
|
+
id: ClientId;
|
|
37
|
+
name: string;
|
|
38
|
+
restart: string;
|
|
39
|
+
};
|
|
40
|
+
/** A client configured by editing its JSON file. */
|
|
41
|
+
export type FileTarget = Base & {
|
|
42
|
+
kind: "file";
|
|
43
|
+
file: string;
|
|
44
|
+
key: "mcpServers";
|
|
45
|
+
};
|
|
46
|
+
/** A client configured through its own CLI's mcp add. */
|
|
47
|
+
export type CliTarget = Base & {
|
|
48
|
+
kind: "cli";
|
|
49
|
+
bin: string; /** where its user config is, read only to keep the caps already set */
|
|
50
|
+
existing?: {
|
|
51
|
+
file: string;
|
|
52
|
+
format: "json" | "toml";
|
|
53
|
+
key?: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** A client init can configure. */
|
|
57
|
+
export type Target = FileTarget | CliTarget;
|
|
58
|
+
/** The target for one client, and whether it looks installed here. */
|
|
59
|
+
export declare function targetFor(id: ClientId, ctx: ClientContext): Promise<{
|
|
60
|
+
target: Target;
|
|
61
|
+
found: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
/** The clients installed on this machine, in CLIENT_IDS order. */
|
|
64
|
+
export declare function detectClients(ctx: ClientContext): Promise<Target[]>;
|
|
65
|
+
/** PATH lookup without running anything. */
|
|
66
|
+
export declare function pathWhich(env: Env, platform: NodeJS.Platform): (bin: string) => Promise<string | null>;
|
|
67
|
+
/** Was this existing entry the hosted URL (which cannot pay)? */
|
|
68
|
+
export declare const isHostedEntry: (e: unknown) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Our entry over an existing one: command and args are ours, the env keeps what the person set (their caps win over
|
|
71
|
+
* the defaults, other keys stay) and takes ours for everything else.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mergeEntry(existing: unknown, ours: ServerEntry): ServerEntry;
|
|
74
|
+
/** What writing a config file would do, or why it is refused. */
|
|
75
|
+
export type FilePlan = {
|
|
76
|
+
ok: true;
|
|
77
|
+
action: "create" | "add" | "replace";
|
|
78
|
+
wasHosted: boolean;
|
|
79
|
+
entry: ServerEntry;
|
|
80
|
+
text: string;
|
|
81
|
+
others: number;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
reason: string;
|
|
85
|
+
};
|
|
86
|
+
/** The new file text for a config, or why it is left alone. `text` null means the file does not exist. */
|
|
87
|
+
export declare function planJson(text: string | null, key: string, ours: ServerEntry): FilePlan;
|
|
88
|
+
/** The file text, or null when it does not exist; other errors are thrown. */
|
|
89
|
+
export declare function readIfExists(file: string): Promise<string | null>;
|
|
90
|
+
/** Where the one-time backup of a config goes. */
|
|
91
|
+
export declare const backupPath: (file: string) => string;
|
|
92
|
+
/**
|
|
93
|
+
* Writes `text` to `file`: a symlink is followed and the file it points at is written; the original is copied to
|
|
94
|
+
* <file>.paidfor-bak the first time (an existing backup is never overwritten); the write is a temp file and a rename,
|
|
95
|
+
* with the file's mode kept (a new file is 600, since it may hold the team key). Returns whether a backup was made.
|
|
96
|
+
*/
|
|
97
|
+
export declare function writeConfigAtomic(file: string, text: string, o?: {
|
|
98
|
+
secret?: boolean;
|
|
99
|
+
}): Promise<{
|
|
100
|
+
backup: string | null;
|
|
101
|
+
tightened: boolean;
|
|
102
|
+
}>;
|
|
103
|
+
/** The env of an existing paidfor entry in a CLI client's own config, best effort (only to keep the caps). */
|
|
104
|
+
export declare function existingCliEnv(t: CliTarget): Promise<Record<string, string>>;
|
|
105
|
+
/** The argv for a CLI client: the remove that clears an old entry (its failure is fine), then the add. */
|
|
106
|
+
export declare function cliCommands(t: CliTarget, entry: ServerEntry): {
|
|
107
|
+
remove?: string[];
|
|
108
|
+
add: string[];
|
|
109
|
+
};
|
|
110
|
+
/** A command as it may be shown: secret values masked. */
|
|
111
|
+
export declare function maskedCommand(bin: string, args: string[]): string;
|
|
112
|
+
/** Render the stdio block to paste by hand, for a client init could not find. Secret values are placeholders. */
|
|
113
|
+
export declare function pasteBlock(entry: ServerEntry): string;
|
|
114
|
+
export {};
|
package/dist/clients.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP clients `paidfor-mcp init` can configure: where each keeps its servers, how to tell it is installed, and how
|
|
3
|
+
* to put the stdio `paidfor` server in it. Formats follow lib/install.ts on the site (checked against each client's
|
|
4
|
+
* docs). File clients are edited in place: merged, backed up once, written atomically, mode kept. CLI clients are
|
|
5
|
+
* driven through their own `mcp add`, which is the only way to give them env; that command line carries the team key,
|
|
6
|
+
* so it is never printed.
|
|
7
|
+
*/
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { access, chmod, copyFile, mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { delimiter, dirname, join } from "node:path";
|
|
11
|
+
/** The clients init knows, in the order they are found and listed. */
|
|
12
|
+
export const CLIENT_IDS = ["claude-code", "codex", "cursor", "claude-desktop", "windsurf", "vscode"];
|
|
13
|
+
/** Whether a --client value names a known client. */
|
|
14
|
+
export const isClientId = (s) => CLIENT_IDS.includes(s);
|
|
15
|
+
/** The env keys whose values are secrets: never printed, masked in any command shown. */
|
|
16
|
+
export const SECRET_ENV = ["PAIDFOR_TEAM_KEY", "PAIDFOR_WALLET_KEY"];
|
|
17
|
+
/** Values a person may have tuned in an existing entry; init keeps them. */
|
|
18
|
+
export const CAP_ENV = ["PAIDFOR_MAX_PER_CALL_USD", "PAIDFOR_DAILY_CAP_USD"];
|
|
19
|
+
const NAMES = {
|
|
20
|
+
"claude-code": "Claude Code",
|
|
21
|
+
codex: "Codex CLI",
|
|
22
|
+
cursor: "Cursor",
|
|
23
|
+
"claude-desktop": "Claude Desktop",
|
|
24
|
+
windsurf: "Windsurf / Devin Desktop",
|
|
25
|
+
vscode: "VS Code",
|
|
26
|
+
};
|
|
27
|
+
const RESTART = {
|
|
28
|
+
"claude-code": "start a new session (or run /mcp) so the tools load",
|
|
29
|
+
codex: "start a new Codex session",
|
|
30
|
+
cursor: "Cursor picks it up; if the tools do not show, toggle the server in Settings, MCP",
|
|
31
|
+
"claude-desktop": "quit and restart Claude Desktop",
|
|
32
|
+
windsurf: "press refresh in Cascade's MCP settings",
|
|
33
|
+
vscode: "run MCP: List Servers, start paidfor, and trust it when asked",
|
|
34
|
+
};
|
|
35
|
+
/** Windows' roaming AppData, from APPDATA or its usual place under the home. */
|
|
36
|
+
const appData = (ctx) => ctx.env.APPDATA ?? join(ctx.home, "AppData", "Roaming");
|
|
37
|
+
/** Whether a path exists. */
|
|
38
|
+
async function exists(p) {
|
|
39
|
+
return stat(p).then(() => true, () => false);
|
|
40
|
+
}
|
|
41
|
+
/** Claude Desktop's config directory on this platform. */
|
|
42
|
+
function claudeDesktopDir(ctx) {
|
|
43
|
+
if (ctx.platform === "darwin")
|
|
44
|
+
return join(ctx.home, "Library", "Application Support", "Claude");
|
|
45
|
+
if (ctx.platform === "win32")
|
|
46
|
+
return join(appData(ctx), "Claude");
|
|
47
|
+
return join(ctx.home, ".config", "Claude");
|
|
48
|
+
}
|
|
49
|
+
/** VS Code's user settings directory on this platform. */
|
|
50
|
+
function vscodeUserDir(ctx) {
|
|
51
|
+
if (ctx.platform === "darwin")
|
|
52
|
+
return join(ctx.home, "Library", "Application Support", "Code", "User");
|
|
53
|
+
if (ctx.platform === "win32")
|
|
54
|
+
return join(appData(ctx), "Code", "User");
|
|
55
|
+
return join(ctx.home, ".config", "Code", "User");
|
|
56
|
+
}
|
|
57
|
+
/** Windsurf (now Devin Desktop) moved its config; the newer directory wins when both exist. */
|
|
58
|
+
async function windsurfFile(ctx) {
|
|
59
|
+
const devin = ctx.platform === "win32" ? join(appData(ctx), "devin") : join(ctx.home, ".config", "devin");
|
|
60
|
+
const older = join(ctx.home, ".codeium", "windsurf");
|
|
61
|
+
if (await exists(devin))
|
|
62
|
+
return { file: join(devin, "mcp_config.json"), found: true };
|
|
63
|
+
if (await exists(older))
|
|
64
|
+
return { file: join(older, "mcp_config.json"), found: true };
|
|
65
|
+
return { file: join(devin, "mcp_config.json"), found: false };
|
|
66
|
+
}
|
|
67
|
+
/** The target for one client, and whether it looks installed here. */
|
|
68
|
+
export async function targetFor(id, ctx) {
|
|
69
|
+
const base = { id, name: NAMES[id], restart: RESTART[id] };
|
|
70
|
+
switch (id) {
|
|
71
|
+
case "claude-code":
|
|
72
|
+
return { target: { ...base, kind: "cli", bin: "claude", existing: { file: join(ctx.home, ".claude.json"), format: "json", key: "mcpServers" } }, found: !!(await ctx.which("claude")) };
|
|
73
|
+
case "codex": {
|
|
74
|
+
const dir = ctx.env.CODEX_HOME?.trim() || join(ctx.home, ".codex");
|
|
75
|
+
return { target: { ...base, kind: "cli", bin: "codex", existing: { file: join(dir, "config.toml"), format: "toml" } }, found: !!(await ctx.which("codex")) };
|
|
76
|
+
}
|
|
77
|
+
case "vscode":
|
|
78
|
+
return { target: { ...base, kind: "cli", bin: "code", existing: { file: join(vscodeUserDir(ctx), "mcp.json"), format: "json", key: "servers" } }, found: !!(await ctx.which("code")) };
|
|
79
|
+
case "cursor": {
|
|
80
|
+
const dir = join(ctx.home, ".cursor");
|
|
81
|
+
return { target: { ...base, kind: "file", file: join(dir, "mcp.json"), key: "mcpServers" }, found: await exists(dir) };
|
|
82
|
+
}
|
|
83
|
+
case "claude-desktop": {
|
|
84
|
+
const dir = claudeDesktopDir(ctx);
|
|
85
|
+
return { target: { ...base, kind: "file", file: join(dir, "claude_desktop_config.json"), key: "mcpServers" }, found: await exists(dir) };
|
|
86
|
+
}
|
|
87
|
+
case "windsurf": {
|
|
88
|
+
const w = await windsurfFile(ctx);
|
|
89
|
+
return { target: { ...base, kind: "file", file: w.file, key: "mcpServers" }, found: w.found };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The clients installed on this machine, in CLIENT_IDS order. */
|
|
94
|
+
export async function detectClients(ctx) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const id of CLIENT_IDS) {
|
|
97
|
+
const t = await targetFor(id, ctx);
|
|
98
|
+
if (t.found)
|
|
99
|
+
out.push(t.target);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** PATH lookup without running anything. */
|
|
104
|
+
export function pathWhich(env, platform) {
|
|
105
|
+
return async (bin) => {
|
|
106
|
+
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
107
|
+
for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
108
|
+
for (const ext of exts) {
|
|
109
|
+
const p = join(dir, bin + ext);
|
|
110
|
+
try {
|
|
111
|
+
await access(p, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
112
|
+
if ((await stat(p)).isFile())
|
|
113
|
+
return p;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* not here */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** A plain JSON object (not null, not an array). */
|
|
124
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
125
|
+
/** The string values of an env object; anything else is dropped. */
|
|
126
|
+
function stringEnv(v) {
|
|
127
|
+
const out = {};
|
|
128
|
+
if (isObj(v))
|
|
129
|
+
for (const [k, x] of Object.entries(v))
|
|
130
|
+
if (typeof x === "string")
|
|
131
|
+
out[k] = x;
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** Was this existing entry the hosted URL (which cannot pay)? */
|
|
135
|
+
export const isHostedEntry = (e) => isObj(e) && (typeof e.url === "string" || typeof e.serverUrl === "string");
|
|
136
|
+
/**
|
|
137
|
+
* Our entry over an existing one: command and args are ours, the env keeps what the person set (their caps win over
|
|
138
|
+
* the defaults, other keys stay) and takes ours for everything else.
|
|
139
|
+
*/
|
|
140
|
+
export function mergeEntry(existing, ours) {
|
|
141
|
+
const old = stringEnv(isObj(existing) ? existing.env : undefined);
|
|
142
|
+
const keptCaps = Object.fromEntries(CAP_ENV.filter((k) => old[k]?.trim()).map((k) => [k, old[k]]));
|
|
143
|
+
return { command: ours.command, args: [...ours.args], env: { ...old, ...ours.env, ...keptCaps } };
|
|
144
|
+
}
|
|
145
|
+
/** The file's own indentation, so a rewrite keeps its look. */
|
|
146
|
+
function indentOf(text) {
|
|
147
|
+
const m = /\n([ \t]+)\S/.exec(text);
|
|
148
|
+
if (!m)
|
|
149
|
+
return 2;
|
|
150
|
+
return m[1].includes("\t") ? "\t" : m[1].length;
|
|
151
|
+
}
|
|
152
|
+
/** The new file text for a config, or why it is left alone. `text` null means the file does not exist. */
|
|
153
|
+
export function planJson(text, key, ours) {
|
|
154
|
+
let doc = {};
|
|
155
|
+
if (text !== null && text.trim() !== "") {
|
|
156
|
+
try {
|
|
157
|
+
doc = JSON.parse(text);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { ok: false, reason: "it is not valid JSON (comments or a trailing comma?)" };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!isObj(doc))
|
|
164
|
+
return { ok: false, reason: "it does not hold a JSON object" };
|
|
165
|
+
const servers = doc[key] ?? {};
|
|
166
|
+
if (!isObj(servers))
|
|
167
|
+
return { ok: false, reason: `its "${key}" is not an object` };
|
|
168
|
+
const prev = servers.paidfor;
|
|
169
|
+
const entry = mergeEntry(prev, ours);
|
|
170
|
+
const next = { ...doc, [key]: { ...servers, paidfor: entry } };
|
|
171
|
+
const others = Object.keys(servers).filter((k) => k !== "paidfor").length;
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
action: text === null ? "create" : prev === undefined ? "add" : "replace",
|
|
175
|
+
wasHosted: isHostedEntry(prev),
|
|
176
|
+
entry,
|
|
177
|
+
text: JSON.stringify(next, null, text ? indentOf(text) : 2) + "\n",
|
|
178
|
+
others,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The file text, or null when it does not exist; other errors are thrown. */
|
|
182
|
+
export async function readIfExists(file) {
|
|
183
|
+
try {
|
|
184
|
+
return await readFile(file, "utf8");
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
if (e.code === "ENOENT")
|
|
188
|
+
return null;
|
|
189
|
+
throw e;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Where the one-time backup of a config goes. */
|
|
193
|
+
export const backupPath = (file) => `${file}.paidfor-bak`;
|
|
194
|
+
/**
|
|
195
|
+
* Writes `text` to `file`: a symlink is followed and the file it points at is written; the original is copied to
|
|
196
|
+
* <file>.paidfor-bak the first time (an existing backup is never overwritten); the write is a temp file and a rename,
|
|
197
|
+
* with the file's mode kept (a new file is 600, since it may hold the team key). Returns whether a backup was made.
|
|
198
|
+
*/
|
|
199
|
+
export async function writeConfigAtomic(file, text, o = {}) {
|
|
200
|
+
let real = file;
|
|
201
|
+
let mode = 0o600;
|
|
202
|
+
let existed = false;
|
|
203
|
+
try {
|
|
204
|
+
real = await realpath(file);
|
|
205
|
+
mode = (await stat(real)).mode & 0o7777;
|
|
206
|
+
existed = true;
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
if (e.code !== "ENOENT")
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
212
|
+
let backup = null;
|
|
213
|
+
if (existed) {
|
|
214
|
+
const bak = backupPath(file);
|
|
215
|
+
try {
|
|
216
|
+
if (o.secret) {
|
|
217
|
+
// The file being replaced may already hold the key: its backup is owner-only from the moment it exists.
|
|
218
|
+
await writeFile(bak, await readFile(real), { flag: "wx", mode: mode & ~0o077 });
|
|
219
|
+
await chmod(bak, mode & ~0o077);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
await copyFile(real, bak, constants.COPYFILE_EXCL);
|
|
223
|
+
await chmod(bak, mode);
|
|
224
|
+
}
|
|
225
|
+
backup = bak;
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
if (e.code !== "EEXIST")
|
|
229
|
+
throw e;
|
|
230
|
+
// A backup from an earlier run may hold the key too. If it cannot be made owner-only, say so and write nothing.
|
|
231
|
+
if (o.secret) {
|
|
232
|
+
const bakMode = (await stat(bak)).mode & 0o7777;
|
|
233
|
+
if ((bakMode & 0o077) !== 0) {
|
|
234
|
+
await chmod(bak, bakMode & ~0o077).catch(() => {
|
|
235
|
+
throw new Error(`the earlier backup ${bak} may hold the account's key and could not be made readable by you only; fix its permissions or remove it, then run this again`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
await mkdir(dirname(real), { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
// A config that carries the team key is readable by its owner only, like the wallet key file.
|
|
245
|
+
const tightened = !!o.secret && (mode & 0o077) !== 0;
|
|
246
|
+
if (o.secret)
|
|
247
|
+
mode &= ~0o077;
|
|
248
|
+
const tmp = `${real}.paidfor-tmp-${process.pid}`;
|
|
249
|
+
try {
|
|
250
|
+
await writeFile(tmp, text, { mode });
|
|
251
|
+
await chmod(tmp, mode);
|
|
252
|
+
await rename(tmp, real);
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
await unlink(tmp).catch(() => undefined);
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
return { backup, tightened };
|
|
259
|
+
}
|
|
260
|
+
/** The env of an existing paidfor entry in a CLI client's own config, best effort (only to keep the caps). */
|
|
261
|
+
export async function existingCliEnv(t) {
|
|
262
|
+
if (!t.existing)
|
|
263
|
+
return {};
|
|
264
|
+
const text = await readIfExists(t.existing.file).catch(() => null);
|
|
265
|
+
if (!text)
|
|
266
|
+
return {};
|
|
267
|
+
if (t.existing.format === "json") {
|
|
268
|
+
try {
|
|
269
|
+
const doc = JSON.parse(text);
|
|
270
|
+
const servers = doc[t.existing.key ?? "mcpServers"];
|
|
271
|
+
return isObj(servers) && isObj(servers.paidfor) ? stringEnv(servers.paidfor.env) : {};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// TOML: the [mcp_servers.paidfor.env] table, or an inline `env = { ... }` under [mcp_servers.paidfor].
|
|
278
|
+
const out = {};
|
|
279
|
+
const lines = text.split(/\r?\n/);
|
|
280
|
+
let section = "";
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const h = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
283
|
+
if (h) {
|
|
284
|
+
section = h[1].trim();
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (section === "mcp_servers.paidfor.env") {
|
|
288
|
+
const m = /^\s*"?([A-Za-z_][A-Za-z0-9_]*)"?\s*=\s*"([^"]*)"/.exec(line);
|
|
289
|
+
if (m)
|
|
290
|
+
out[m[1]] = m[2];
|
|
291
|
+
}
|
|
292
|
+
else if (section === "mcp_servers.paidfor") {
|
|
293
|
+
const m = /^\s*env\s*=\s*\{(.*)\}\s*$/.exec(line);
|
|
294
|
+
if (m)
|
|
295
|
+
for (const p of m[1].matchAll(/"?([A-Za-z_][A-Za-z0-9_]*)"?\s*=\s*"([^"]*)"/g))
|
|
296
|
+
out[p[1]] = p[2];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
/** The argv for a CLI client: the remove that clears an old entry (its failure is fine), then the add. */
|
|
302
|
+
export function cliCommands(t, entry) {
|
|
303
|
+
const envPairs = Object.entries(entry.env).map(([k, v]) => `${k}=${v}`);
|
|
304
|
+
if (t.id === "claude-code") {
|
|
305
|
+
// The name goes before -e: -e takes several values and would swallow it.
|
|
306
|
+
return { remove: ["mcp", "remove", "paidfor", "--scope", "user"], add: ["mcp", "add", "paidfor", "--scope", "user", ...envPairs.flatMap((p) => ["-e", p]), "--", entry.command, ...entry.args] };
|
|
307
|
+
}
|
|
308
|
+
if (t.id === "codex") {
|
|
309
|
+
return { remove: ["mcp", "remove", "paidfor"], add: ["mcp", "add", "paidfor", ...envPairs.flatMap((p) => ["--env", p]), "--", entry.command, ...entry.args] };
|
|
310
|
+
}
|
|
311
|
+
// VS Code: --add-mcp takes the server as JSON and puts it in the user profile's mcp.json.
|
|
312
|
+
return { add: ["--add-mcp", JSON.stringify({ name: "paidfor", command: entry.command, args: entry.args, env: entry.env })] };
|
|
313
|
+
}
|
|
314
|
+
/** A command as it may be shown: secret values masked. */
|
|
315
|
+
export function maskedCommand(bin, args) {
|
|
316
|
+
/** One argument with the secret env values masked. */
|
|
317
|
+
const mask = (s) => {
|
|
318
|
+
let out = s;
|
|
319
|
+
for (const k of SECRET_ENV) {
|
|
320
|
+
out = out.replace(new RegExp(`(${k}=)[^\\s"']+`, "g"), "$1***").replace(new RegExp(`("${k}":")[^"]*`, "g"), "$1***");
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
};
|
|
324
|
+
return [bin, ...args.map((a) => (/[\s"'{}]/.test(a) ? `'${mask(a)}'` : mask(a)))].join(" ");
|
|
325
|
+
}
|
|
326
|
+
/** Render the stdio block to paste by hand, for a client init could not find. Secret values are placeholders. */
|
|
327
|
+
export function pasteBlock(entry) {
|
|
328
|
+
const env = Object.fromEntries(Object.entries(entry.env).map(([k, v]) => [k, SECRET_ENV.includes(k) ? `<${k}>` : v]));
|
|
329
|
+
return JSON.stringify({ mcpServers: { paidfor: { command: entry.command, args: entry.args, ...(Object.keys(env).length ? { env } : {}) } } }, null, 2);
|
|
330
|
+
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Exec } from "./clients.js";
|
|
2
|
+
import { type Env } from "./wallet.js";
|
|
3
|
+
/** The package as the configs pin it: this exact version, never latest. */
|
|
4
|
+
export declare const PKG = "@paidfor/mcp@0.7.0";
|
|
5
|
+
/** What init and link send as X-Paidfor-Client and as the link's `client`, so the funnel can be counted. */
|
|
6
|
+
export declare const INIT_CLIENT = "mcp-init/0.7.0";
|
|
7
|
+
/** How long `link` in --yes mode checks a resumed link before it exits and leaves the record for the next run. */
|
|
8
|
+
export declare const RESUME_WINDOW_MS = 20000;
|
|
9
|
+
/** Everything runInit touches outside itself, injected so the tests can fake the home, the server and the client CLIs. */
|
|
10
|
+
export type InitDeps = {
|
|
11
|
+
env: Env;
|
|
12
|
+
home: string;
|
|
13
|
+
platform: NodeJS.Platform;
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** stdin is a terminal a person can answer on. */
|
|
16
|
+
interactive: boolean;
|
|
17
|
+
/** stdout is a terminal (the browser is only opened for one). */
|
|
18
|
+
stdoutTTY: boolean;
|
|
19
|
+
out: (s: string) => void;
|
|
20
|
+
err: (s: string) => void;
|
|
21
|
+
prompt: (question: string) => Promise<string>;
|
|
22
|
+
fetch: typeof fetch;
|
|
23
|
+
exec: Exec;
|
|
24
|
+
which: (bin: string) => Promise<string | null>;
|
|
25
|
+
sleep: (ms: number) => Promise<void>;
|
|
26
|
+
now: () => number;
|
|
27
|
+
openBrowser: (url: string) => void;
|
|
28
|
+
hostname: string;
|
|
29
|
+
};
|
|
30
|
+
/** The parsed command line; null means the flag was not given. */
|
|
31
|
+
type Flags = {
|
|
32
|
+
yes: boolean;
|
|
33
|
+
dryRun: boolean;
|
|
34
|
+
wait: boolean;
|
|
35
|
+
clients: string[] | null;
|
|
36
|
+
wallet: boolean | null;
|
|
37
|
+
link: boolean | null;
|
|
38
|
+
keyFile: string | null;
|
|
39
|
+
label: string | null;
|
|
40
|
+
};
|
|
41
|
+
/** Printed with any usage error. */
|
|
42
|
+
export declare const INIT_USAGE: string;
|
|
43
|
+
/** Parses the init or link command line; throws UsageError on an unknown or misused flag. */
|
|
44
|
+
export declare function parseFlags(argv: string[], mode: "init" | "link"): Flags;
|
|
45
|
+
/** Runs `paidfor-mcp init` or `paidfor-mcp link`. Returns the exit code. */
|
|
46
|
+
export declare function runInit(argv: string[], d: InitDeps, mode?: "init" | "link"): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* One argument for a cmd.exe command line. First quoted the way the Windows C runtime splits arguments (backslashes
|
|
49
|
+
* before a quote doubled, quotes escaped), then every character cmd.exe gives a meaning to is escaped with ^. The CLIs
|
|
50
|
+
* are .cmd shims, and a shim passes its arguments through cmd.exe a second time, so the ^ escaping is applied twice.
|
|
51
|
+
* The same rules as cross-spawn.
|
|
52
|
+
*/
|
|
53
|
+
export declare function winArg(arg: string): string;
|
|
54
|
+
/** What to spawn on Windows for a CLI shim: cmd.exe with one verbatim, fully escaped command line. */
|
|
55
|
+
export declare function winSpawn(cmd: string, args: string[], env: Env): {
|
|
56
|
+
file: string;
|
|
57
|
+
args: string[];
|
|
58
|
+
};
|
|
59
|
+
/** The real world: process env, this home, a TTY prompt, node's fetch, child processes. */
|
|
60
|
+
export declare function realDeps(): InitDeps;
|
|
61
|
+
export {};
|