settld 0.2.2 → 0.2.4

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 CHANGED
@@ -73,6 +73,16 @@ Agent host onboarding (Codex / Claude / Cursor / OpenClaw), with guided wallet +
73
73
  npx -y settld setup
74
74
  ```
75
75
 
76
+ Default interactive flow is now login-first:
77
+
78
+ 1. pick host + wallet mode
79
+ 2. choose `quick` setup (recommended)
80
+ 3. login with OTP (creates tenant if needed)
81
+ 4. setup mints runtime API key automatically
82
+ 5. guided wallet fund + first paid call check runs
83
+
84
+ Advanced mode is still available in setup when you need explicit base URL/bootstrap/API-key control.
85
+
76
86
  Preflight-only check (no host config write), with JSON report:
77
87
 
78
88
  ```sh
@@ -89,6 +99,7 @@ settld setup
89
99
  Check wallet wiring and funding path:
90
100
 
91
101
  ```sh
102
+ settld login
92
103
  settld wallet status
93
104
  settld wallet fund --open
94
105
  settld wallet fund --method transfer
@@ -13,16 +13,27 @@ For deeper tool-level examples, see `docs/QUICKSTART_MCP.md`.
13
13
 
14
14
  ## 1) Before you run `settld setup`
15
15
 
16
- Required inputs:
16
+ Public default path (recommended):
17
17
 
18
- - `SETTLD_BASE_URL` (local or hosted API URL)
19
- - `SETTLD_TENANT_ID`
18
+ - Node.js 20+
19
+ - no API keys required up front
20
+ - run `settld setup`, choose `quick`, then login with OTP
21
+ - setup creates tenant (if needed), mints runtime key, and wires MCP
22
+
23
+ Admin/operator path (advanced):
24
+
25
+ - explicit `SETTLD_BASE_URL`, `SETTLD_TENANT_ID`
20
26
  - one of:
21
27
  - `SETTLD_API_KEY` (`keyId.secret`), or
22
- - `SETTLD_BOOTSTRAP_API_KEY` (onboarding bootstrap key that mints `SETTLD_API_KEY` during setup)
23
- - Node.js 20+
28
+ - `SETTLD_BOOTSTRAP_API_KEY` (bootstrap key that mints runtime key)
29
+
30
+ Recommended interactive pattern:
31
+
32
+ ```bash
33
+ settld setup
34
+ ```
24
35
 
25
- Recommended non-interactive pattern:
36
+ Recommended non-interactive pattern (automation/support):
26
37
 
27
38
  ```bash
28
39
  settld setup --non-interactive \
@@ -37,7 +48,7 @@ settld setup --non-interactive \
37
48
  --out-env ./.tmp/settld-openclaw.env
38
49
  ```
39
50
 
40
- If you want setup to generate the tenant API key for you:
51
+ If you want non-interactive setup to generate the tenant API key:
41
52
 
42
53
  ```bash
43
54
  settld setup --non-interactive \
@@ -73,14 +84,18 @@ Unified setup command:
73
84
  settld setup
74
85
  ```
75
86
 
76
- The wizard handles:
87
+ `quick` mode (default) handles:
77
88
 
78
89
  - host selection (`codex|claude|cursor|openclaw`)
79
90
  - wallet mode selection (`managed|byo|none`)
91
+ - login/signup + OTP session flow (no manual key paste)
80
92
  - preflight checks (API health, tenant auth, profile baseline, host config path)
81
93
  - policy apply + optional smoke
94
+ - guided wallet funding and first paid MCP check
82
95
  - interactive menus with arrow keys (Up/Down + Enter) for choice steps
83
96
 
97
+ `advanced` mode exposes explicit key/bootstrap/base-url prompts and fine-grained setup toggles.
98
+
84
99
  Host-specific non-interactive examples:
85
100
 
86
101
  ```bash
@@ -177,6 +192,12 @@ Check wallet assignment after setup:
177
192
  settld wallet status
178
193
  ```
179
194
 
195
+ If wallet commands return auth errors, run:
196
+
197
+ ```bash
198
+ settld login
199
+ ```
200
+
180
201
  Funding paths:
181
202
 
182
203
  ```bash
package/docs/README.md CHANGED
@@ -17,10 +17,11 @@ For curated public docs, start here:
17
17
 
18
18
  ## Fastest onboarding path
19
19
 
20
- 1. Run `settld setup` (or `./bin/settld.js setup`) with your host, tenant, and API key.
21
- 2. Activate your host and run `npm run mcp:probe`.
22
- 3. Run `npm run demo:mcp-paid-exa`.
23
- 4. Verify the first receipt:
20
+ 1. Run `settld setup` (or `./bin/settld.js setup`), choose `quick`, and complete OTP login.
21
+ 2. Let guided wallet funding complete (or run `settld wallet fund` + `settld wallet balance --watch --min-usdc 1`).
22
+ 3. Activate your host and run `npm run mcp:probe`.
23
+ 4. Run `npm run demo:mcp-paid-exa`.
24
+ 5. Verify the first receipt:
24
25
 
25
26
  ```bash
26
27
  jq -c 'first' artifacts/mcp-paid-exa/*/x402-receipts.export.jsonl > /tmp/settld-first-receipt.json
@@ -19,9 +19,13 @@ Settld gives you a canonical economic loop:
19
19
  ## One-command onboarding
20
20
 
21
21
  ```bash
22
- settld setup --non-interactive --host codex --base-url http://127.0.0.1:3000 --tenant-id tenant_default --settld-api-key sk_live_xxx.yyy --wallet-mode managed --wallet-bootstrap remote --profile-id engineering-spend --smoke
22
+ settld setup
23
23
  ```
24
24
 
25
+ Recommended path: choose `quick`, complete OTP login, and let setup run guided funding + paid-call checks.
26
+
27
+ Advanced/scripted path is still supported with explicit non-interactive flags.
28
+
25
29
  Then:
26
30
 
27
31
  1. `npm run mcp:probe -- --call settld.about '{}'`
@@ -5,9 +5,8 @@ Get from zero to a verified paid agent action in minutes.
5
5
  ## Prerequisites
6
6
 
7
7
  - Node.js 20+
8
- - Settld API URL
9
- - Tenant ID
10
- - Tenant API key (`keyId.secret`)
8
+ - Public flow: no API key required up front (`settld setup` handles login/session bootstrap)
9
+ - Advanced flow: optional explicit `--base-url`, `--tenant-id`, and `--settld-api-key`
11
10
 
12
11
  ## 0) One-command setup
13
12
 
@@ -17,7 +16,14 @@ Run guided setup:
17
16
  settld setup
18
17
  ```
19
18
 
20
- The guided setup uses arrow-key menus for host/wallet/policy decisions, then asks only the next required fields.
19
+ Recommended interactive choices:
20
+
21
+ 1. host
22
+ 2. `quick` setup mode
23
+ 3. wallet mode
24
+ 4. OTP login (creates tenant if needed)
25
+
26
+ `quick` mode auto-runs preflight/smoke/profile apply, then starts guided wallet fund + first paid call checks.
21
27
 
22
28
  Non-interactive example:
23
29
 
@@ -54,6 +60,7 @@ Then restart your host app (Codex/Claude/Cursor/OpenClaw) so it reloads MCP conf
54
60
  ## 2) Check wallet and fund it
55
61
 
56
62
  ```bash
63
+ settld login
57
64
  settld wallet status
58
65
  settld wallet fund --method transfer
59
66
  settld wallet balance --watch --min-usdc 1
@@ -5,7 +5,7 @@ Copy/paste adoption templates and guardrails:
5
5
  - `github-actions.md` — composite action usage and trust anchor wiring.
6
6
  - `github-actions-verify.yml` — pasteable workflow template.
7
7
  - `openclaw/PUBLIC_QUICKSTART.md` — public npm onboarding flow for OpenClaw (`npx settld@latest setup`).
8
- - `openclaw/settld-mcp-skill/SKILL.md` — OpenClaw skill payload for Settld MCP.
8
+ - `openclaw/settld-mcp-skill/SKILL.md` + `openclaw/settld-mcp-skill/skill.json` — OpenClaw skill payload + manifest for Settld MCP.
9
9
  - `openclaw/CLAWHUB_PUBLISH_CHECKLIST.md` — publish + validation checklist for ClawHub.
10
10
 
11
11
  See also:
@@ -15,6 +15,7 @@ Confirm required files exist:
15
15
 
16
16
  - `docs/integrations/openclaw/settld-mcp-skill/SKILL.md`
17
17
  - `docs/integrations/openclaw/settld-mcp-skill/mcp-server.example.json`
18
+ - `docs/integrations/openclaw/settld-mcp-skill/skill.json`
18
19
 
19
20
  ## 2) Prepare Skill Metadata
20
21
 
@@ -62,4 +63,3 @@ Capture these fields each publish:
62
63
  - Added/changed tools
63
64
  - Known limitations
64
65
  - Validation run timestamp
65
-
@@ -33,10 +33,10 @@ npx -y settld@latest setup
33
33
  Choose:
34
34
 
35
35
  1. `host`: `openclaw`
36
- 2. wallet mode (`managed` recommended first)
37
- 3. wallet bootstrap (`remote` recommended for first setup)
38
- 4. keep preflight + smoke enabled
39
- 5. apply a starter profile (`engineering-spend`)
36
+ 2. setup mode: `quick`
37
+ 3. wallet mode (`managed` recommended first)
38
+ 4. login with OTP (new tenant is created if needed)
39
+ 5. let setup run guided fund + first paid check
40
40
 
41
41
  Non-interactive path (automation/support):
42
42
 
@@ -53,7 +53,7 @@ npx -y settld@latest setup \
53
53
  --smoke
54
54
  ```
55
55
 
56
- If you do not have a tenant `sk_*` yet, let setup mint one:
56
+ Advanced non-interactive key/bootstrap paths are still supported:
57
57
 
58
58
  ```bash
59
59
  npx -y settld@latest setup \
@@ -102,7 +102,9 @@ npx -y settld@latest x402 receipt verify ./receipt.json --format json
102
102
  ## Notes for operators
103
103
 
104
104
  - Public users do not need to clone the Settld repo.
105
+ - Public users should not need bootstrap/admin keys in the default setup path.
105
106
  - Public path is valid only after publishing a package version that includes the current setup flow.
106
107
  - For OpenClaw skill packaging and publish flow, see:
107
108
  - `docs/integrations/openclaw/settld-mcp-skill/SKILL.md`
109
+ - `docs/integrations/openclaw/settld-mcp-skill/skill.json`
108
110
  - `docs/integrations/openclaw/CLAWHUB_PUBLISH_CHECKLIST.md`
@@ -9,6 +9,14 @@ author: Settld
9
9
 
10
10
  This skill teaches OpenClaw agents to use Settld for paid MCP tool calls.
11
11
 
12
+ It is designed for the public `quick` onboarding flow:
13
+
14
+ 1. `settld setup`
15
+ 2. pick `openclaw` + `quick`
16
+ 3. login via OTP
17
+ 4. fund wallet
18
+ 5. run paid tool call with deterministic receipt evidence
19
+
12
20
  ## What This Skill Enables
13
21
 
14
22
  - Discover Settld MCP tools (`settld.*`)
@@ -19,9 +27,7 @@ This skill teaches OpenClaw agents to use Settld for paid MCP tool calls.
19
27
  ## Prerequisites
20
28
 
21
29
  - Node.js 20+
22
- - Settld API key (`SETTLD_API_KEY`)
23
- - Settld API base URL (`SETTLD_BASE_URL`)
24
- - Tenant id (`SETTLD_TENANT_ID`)
30
+ - Settld runtime env from setup (`SETTLD_API_KEY`, `SETTLD_BASE_URL`, `SETTLD_TENANT_ID`)
25
31
  - Optional paid tools base URL (`SETTLD_PAID_TOOLS_BASE_URL`)
26
32
 
27
33
  ## MCP Server Registration
@@ -61,9 +67,18 @@ Optional env vars:
61
67
  - "Call `settld.about` and return the result JSON."
62
68
  - "Run `settld.weather_current_paid` for Chicago in fahrenheit and include the `x-settld-*` headers."
63
69
 
70
+ ## Identity + Traceability
71
+
72
+ Every paid call should be explainable and auditable:
73
+
74
+ - tenant identity (who owns the runtime)
75
+ - actor/session identity (who approved/triggered)
76
+ - policy decision identity (`allow|challenge|deny|escalate` + reason codes)
77
+ - settlement identity (`settlementReceiptId`)
78
+ - evidence identity (hash-verifiable receipt/timeline artifacts)
79
+
64
80
  ## Safety Notes
65
81
 
66
82
  - Treat `SETTLD_API_KEY` as secret input.
67
83
  - Do not print full API keys in chat output.
68
84
  - Keep paid tools scoped to trusted providers and tenant policy.
69
-
@@ -3,10 +3,8 @@
3
3
  "command": "npx",
4
4
  "args": ["-y", "settld-mcp"],
5
5
  "env": {
6
- "SETTLD_BASE_URL": "http://127.0.0.1:3000",
7
- "SETTLD_TENANT_ID": "tenant_default",
8
- "SETTLD_API_KEY": "sk_live_xxx.yyy",
9
- "SETTLD_PAID_TOOLS_BASE_URL": "http://127.0.0.1:8402"
6
+ "SETTLD_BASE_URL": "https://api.settld.work",
7
+ "SETTLD_TENANT_ID": "tenant_xxx",
8
+ "SETTLD_API_KEY": "sk_live_xxx.yyy"
10
9
  }
11
10
  }
12
-
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "settld-mcp-payments",
3
+ "version": "0.2.0",
4
+ "description": "OpenClaw skill for Settld paid MCP tools with policy decisions and verifiable receipts.",
5
+ "author": "Settld",
6
+ "entry": "SKILL.md",
7
+ "files": [
8
+ "SKILL.md",
9
+ "mcp-server.example.json"
10
+ ]
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "settld",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Settld kernel CLI and local control-plane tooling",
5
5
  "private": false,
6
6
  "type": "module",
@@ -39,6 +39,7 @@ function readArgValue(argv, index, rawArg) {
39
39
  function parseArgs(argv) {
40
40
  const out = {
41
41
  baseUrl: "https://api.settld.work",
42
+ baseUrlProvided: false,
42
43
  tenantId: "",
43
44
  email: "",
44
45
  company: "",
@@ -64,6 +65,7 @@ function parseArgs(argv) {
64
65
  if (arg === "--base-url" || arg.startsWith("--base-url=")) {
65
66
  const parsed = readArgValue(argv, i, arg);
66
67
  out.baseUrl = parsed.value;
68
+ out.baseUrlProvided = true;
67
69
  i = parsed.nextIndex;
68
70
  continue;
69
71
  }
@@ -149,6 +151,20 @@ async function requestJson(url, { method, body, headers = {}, fetchImpl = fetch
149
151
  return { res, text, json };
150
152
  }
151
153
 
154
+ function responseCode({ json }) {
155
+ const direct = typeof json?.code === "string" ? json.code : "";
156
+ if (direct) return direct;
157
+ const error = typeof json?.error === "string" ? json.error : "";
158
+ return error ? error.toUpperCase() : "";
159
+ }
160
+
161
+ function responseMessage({ json, text }, fallback = "unknown error") {
162
+ if (typeof json?.message === "string" && json.message.trim()) return json.message.trim();
163
+ if (typeof json?.error === "string" && json.error.trim()) return json.error.trim();
164
+ const raw = String(text ?? "").trim();
165
+ return raw || fallback;
166
+ }
167
+
152
168
  async function promptLine(rl, label, { defaultValue = "", required = true } = {}) {
153
169
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
154
170
  const value = String(await rl.question(`${label}${suffix}: `) ?? "").trim() || String(defaultValue ?? "").trim();
@@ -190,11 +206,16 @@ export async function runLogin({
190
206
  const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
191
207
  try {
192
208
  if (interactive) {
193
- state.baseUrl = await promptLine(rl, "Settld base URL", { defaultValue: state.baseUrl || "https://api.settld.work" });
194
- state.tenantId = await promptLine(rl, "Tenant ID (optional for new signup)", { defaultValue: state.tenantId, required: false });
209
+ if (!args.baseUrlProvided) {
210
+ state.baseUrl = await promptLine(rl, "Settld base URL", { defaultValue: state.baseUrl || "https://api.settld.work" });
211
+ }
212
+ state.tenantId = await promptLine(rl, "Tenant ID (optional, leave blank to create new)", {
213
+ defaultValue: state.tenantId,
214
+ required: false
215
+ });
195
216
  state.email = (await promptLine(rl, "Email", { defaultValue: state.email })).toLowerCase();
196
217
  if (!state.tenantId) {
197
- state.company = await promptLine(rl, "Company name", { defaultValue: state.company });
218
+ state.company = await promptLine(rl, "What should your tenant/company name be?", { defaultValue: state.company });
198
219
  }
199
220
  }
200
221
 
@@ -202,7 +223,20 @@ export async function runLogin({
202
223
  if (!state.email) throw new Error("email is required");
203
224
  if (!state.tenantId && !state.company) throw new Error("company is required when tenant ID is omitted");
204
225
 
226
+ const requestTenantOtp = async (tenantId) => {
227
+ const otpRequest = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(String(tenantId ?? ""))}/buyer/login/otp`, {
228
+ method: "POST",
229
+ body: { email: state.email },
230
+ fetchImpl
231
+ });
232
+ if (!otpRequest.res.ok) {
233
+ const message = responseMessage(otpRequest);
234
+ throw new Error(`otp request failed (${otpRequest.res.status}): ${message}`);
235
+ }
236
+ };
237
+
205
238
  let tenantId = state.tenantId;
239
+ let otpAlreadyIssued = false;
206
240
  if (!tenantId) {
207
241
  const signup = await requestJson(`${baseUrl}/v1/public/signup`, {
208
242
  method: "POST",
@@ -210,27 +244,24 @@ export async function runLogin({
210
244
  fetchImpl
211
245
  });
212
246
  if (!signup.res.ok) {
213
- const code = typeof signup.json?.code === "string" ? signup.json.code : "";
214
- const message = typeof signup.json?.message === "string" ? signup.json.message : signup.text;
247
+ const code = responseCode(signup);
248
+ const message = responseMessage(signup);
215
249
  if (code === "SIGNUP_DISABLED") {
216
250
  throw new Error("Public signup is disabled for this environment. Use an existing tenant ID or bootstrap key flow.");
217
251
  }
218
- throw new Error(`public signup failed (${signup.res.status}): ${message || "unknown error"}`);
252
+ if (signup.res.status === 403 && code === "FORBIDDEN") {
253
+ throw new Error(
254
+ "Public signup is unavailable on this base URL. Retry with --tenant-id <existing_tenant>, or in `settld setup` choose `Generate during setup` and provide an onboarding bootstrap API key."
255
+ );
256
+ }
257
+ throw new Error(`public signup failed (${signup.res.status}): ${message}`);
219
258
  }
220
259
  tenantId = String(signup.json?.tenantId ?? "").trim();
221
260
  if (!tenantId) throw new Error("public signup response missing tenantId");
261
+ otpAlreadyIssued = Boolean(signup.json?.otpIssued);
222
262
  if (interactive) stdout.write(`Created tenant: ${tenantId}\n`);
223
- } else {
224
- const otpRequest = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(tenantId)}/buyer/login/otp`, {
225
- method: "POST",
226
- body: { email: state.email },
227
- fetchImpl
228
- });
229
- if (!otpRequest.res.ok) {
230
- const message = typeof otpRequest.json?.message === "string" ? otpRequest.json.message : otpRequest.text;
231
- throw new Error(`otp request failed (${otpRequest.res.status}): ${message || "unknown error"}`);
232
- }
233
263
  }
264
+ if (!otpAlreadyIssued) await requestTenantOtp(tenantId);
234
265
 
235
266
  if (!state.otp && interactive) {
236
267
  state.otp = await promptLine(rl, "OTP code", { required: true });
@@ -243,8 +274,8 @@ export async function runLogin({
243
274
  fetchImpl
244
275
  });
245
276
  if (!login.res.ok) {
246
- const message = typeof login.json?.message === "string" ? login.json.message : login.text;
247
- throw new Error(`login failed (${login.res.status}): ${message || "unknown error"}`);
277
+ const message = responseMessage(login);
278
+ throw new Error(`login failed (${login.res.status}): ${message}`);
248
279
  }
249
280
  const setCookie = login.res.headers.get("set-cookie") ?? "";
250
281
  const cookie = cookieHeaderFromSetCookie(setCookie);
@@ -13,10 +13,13 @@ import { bootstrapWalletProvider } from "../../src/core/wallet-provider-bootstra
13
13
  import { extractBootstrapMcpEnv, loadHostConfigHelper, runWizard } from "./wizard.mjs";
14
14
  import { SUPPORTED_HOSTS } from "./host-config.mjs";
15
15
  import { defaultSessionPath, readSavedSession } from "./session-store.mjs";
16
+ import { runLogin } from "./login.mjs";
17
+ import { runWalletCli } from "../wallet/cli.mjs";
16
18
 
17
19
  const WALLET_MODES = new Set(["managed", "byo", "none"]);
18
20
  const WALLET_PROVIDERS = new Set(["circle"]);
19
21
  const WALLET_BOOTSTRAP_MODES = new Set(["auto", "local", "remote"]);
22
+ const SETUP_MODES = new Set(["quick", "advanced"]);
20
23
  const FORMAT_OPTIONS = new Set(["text", "json"]);
21
24
  const HOST_BINARY_HINTS = Object.freeze({
22
25
  codex: "codex",
@@ -38,6 +41,7 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
38
41
  const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
39
42
  const SETTLD_BIN = path.join(REPO_ROOT, "bin", "settld.js");
40
43
  const PROFILE_FINGERPRINT_REGEX = /^[0-9a-f]{64}$/;
44
+ const DEFAULT_PUBLIC_BASE_URL = "https://api.settld.work";
41
45
  const ANSI_RESET = "\u001b[0m";
42
46
  const ANSI_BOLD = "\u001b[1m";
43
47
  const ANSI_DIM = "\u001b[2m";
@@ -54,6 +58,10 @@ function usage() {
54
58
  "",
55
59
  "flags:",
56
60
  " --non-interactive Disable prompts; require explicit flags",
61
+ " --quick Quick setup mode (default for interactive)",
62
+ " --advanced Advanced setup mode (shows all prompts)",
63
+ " --guided-next Run guided fund + first paid check after setup (default in quick mode)",
64
+ " --skip-guided-next Skip guided post-setup actions",
57
65
  ` --host <${SUPPORTED_HOSTS.join("|")}> Host target (default: auto-detect, fallback openclaw)`,
58
66
  " --base-url <url> Settld API base URL (or SETTLD_BASE_URL)",
59
67
  " --tenant-id <id> Settld tenant ID (or SETTLD_TENANT_ID)",
@@ -109,6 +117,8 @@ function parseWalletEnvAssignment(raw) {
109
117
  function parseArgs(argv) {
110
118
  const out = {
111
119
  nonInteractive: false,
120
+ setupMode: null,
121
+ guidedNext: null,
112
122
  host: null,
113
123
  baseUrl: null,
114
124
  tenantId: null,
@@ -149,6 +159,22 @@ function parseArgs(argv) {
149
159
  out.nonInteractive = true;
150
160
  continue;
151
161
  }
162
+ if (arg === "--quick") {
163
+ out.setupMode = "quick";
164
+ continue;
165
+ }
166
+ if (arg === "--advanced") {
167
+ out.setupMode = "advanced";
168
+ continue;
169
+ }
170
+ if (arg === "--guided-next") {
171
+ out.guidedNext = true;
172
+ continue;
173
+ }
174
+ if (arg === "--skip-guided-next") {
175
+ out.guidedNext = false;
176
+ continue;
177
+ }
152
178
  if (arg === "--skip-profile-apply") {
153
179
  out.skipProfileApply = true;
154
180
  continue;
@@ -310,6 +336,9 @@ function parseArgs(argv) {
310
336
  if (out.host && !SUPPORTED_HOSTS.includes(out.host)) {
311
337
  throw new Error(`--host must be one of: ${SUPPORTED_HOSTS.join(", ")}`);
312
338
  }
339
+ if (out.setupMode && !SETUP_MODES.has(out.setupMode)) {
340
+ throw new Error("--quick or --advanced are the supported setup modes");
341
+ }
313
342
  if (!WALLET_MODES.has(out.walletMode)) throw new Error("--wallet-mode must be managed|byo|none");
314
343
  if (!WALLET_PROVIDERS.has(out.walletProvider)) throw new Error(`--wallet-provider must be one of: ${[...WALLET_PROVIDERS].join(", ")}`);
315
344
  if (!WALLET_BOOTSTRAP_MODES.has(out.walletBootstrap)) throw new Error("--wallet-bootstrap must be auto|local|remote");
@@ -865,6 +894,144 @@ function buildHostNextSteps({ host, installedHosts }) {
865
894
  return steps;
866
895
  }
867
896
 
897
+ function runMcpPaidCallProbe({ env, timeoutMs = 45_000 } = {}) {
898
+ const args = [
899
+ path.join("scripts", "mcp", "probe.mjs"),
900
+ "--call",
901
+ "settld.weather_current_paid",
902
+ JSON.stringify({ city: "Chicago", unit: "f" }),
903
+ "--timeout-ms",
904
+ String(timeoutMs)
905
+ ];
906
+ const result = spawnSync(process.execPath, args, {
907
+ cwd: REPO_ROOT,
908
+ env: { ...process.env, ...(env ?? {}) },
909
+ encoding: "utf8",
910
+ timeout: timeoutMs + 5000,
911
+ maxBuffer: 4 * 1024 * 1024,
912
+ stdio: ["ignore", "pipe", "pipe"]
913
+ });
914
+ const stdoutText = String(result.stdout ?? "");
915
+ const stderrText = String(result.stderr ?? "");
916
+ const sawToolError = /"isError"\s*:\s*true/.test(stdoutText);
917
+ const sawToolSuccess = /"isError"\s*:\s*false/.test(stdoutText);
918
+ const ok = Number(result.status) === 0 && sawToolSuccess && !sawToolError;
919
+ return {
920
+ ok,
921
+ exitCode: typeof result.status === "number" ? result.status : 1,
922
+ stdout: stdoutText,
923
+ stderr: stderrText
924
+ };
925
+ }
926
+
927
+ async function runGuidedQuickFlow({
928
+ enabled = false,
929
+ walletMode = "none",
930
+ normalizedBaseUrl,
931
+ tenantId,
932
+ sessionFile,
933
+ sessionCookie = "",
934
+ mergedEnv = {},
935
+ runtimeEnv = process.env,
936
+ stdin = process.stdin,
937
+ stdout = process.stdout,
938
+ runWalletCliImpl = runWalletCli
939
+ } = {}) {
940
+ const summary = {
941
+ enabled: Boolean(enabled),
942
+ ran: false,
943
+ walletFund: null,
944
+ walletBalanceWatch: null,
945
+ firstPaidCall: null,
946
+ warnings: []
947
+ };
948
+ if (!summary.enabled) return summary;
949
+ if (!stdin?.isTTY || !stdout?.isTTY) {
950
+ summary.warnings.push("guided quick flow skipped (non-interactive terminal)");
951
+ return summary;
952
+ }
953
+
954
+ const actionEnv = {
955
+ ...runtimeEnv,
956
+ ...mergedEnv,
957
+ SETTLD_BASE_URL: normalizedBaseUrl,
958
+ SETTLD_TENANT_ID: tenantId,
959
+ ...(sessionCookie ? { SETTLD_SESSION_COOKIE: sessionCookie } : {})
960
+ };
961
+ summary.ran = true;
962
+
963
+ if (walletMode !== "none") {
964
+ try {
965
+ const fundResult = await runWalletCliImpl({
966
+ argv: ["fund", "--open", "--base-url", normalizedBaseUrl, "--tenant-id", tenantId, "--session-file", sessionFile, "--format", "json"],
967
+ env: actionEnv,
968
+ stdin,
969
+ stdout
970
+ });
971
+ summary.walletFund = {
972
+ ok: true,
973
+ method: String(fundResult?.method ?? "").trim() || null
974
+ };
975
+ } catch (err) {
976
+ summary.walletFund = {
977
+ ok: false,
978
+ error: err?.message ?? String(err ?? "")
979
+ };
980
+ summary.warnings.push("wallet funding did not complete during guided flow");
981
+ }
982
+
983
+ try {
984
+ const watchResult = await runWalletCliImpl({
985
+ argv: [
986
+ "balance",
987
+ "--watch",
988
+ "--min-usdc",
989
+ "1",
990
+ "--timeout-seconds",
991
+ "300",
992
+ "--interval-seconds",
993
+ "5",
994
+ "--base-url",
995
+ normalizedBaseUrl,
996
+ "--tenant-id",
997
+ tenantId,
998
+ "--session-file",
999
+ sessionFile,
1000
+ "--format",
1001
+ "json"
1002
+ ],
1003
+ env: actionEnv,
1004
+ stdin,
1005
+ stdout
1006
+ });
1007
+ summary.walletBalanceWatch = {
1008
+ ok: Boolean(watchResult?.ok),
1009
+ satisfied: Boolean(watchResult?.watch?.satisfied)
1010
+ };
1011
+ } catch (err) {
1012
+ summary.walletBalanceWatch = {
1013
+ ok: false,
1014
+ error: err?.message ?? String(err ?? "")
1015
+ };
1016
+ summary.warnings.push("wallet balance watch did not reach target within timeout");
1017
+ }
1018
+ }
1019
+
1020
+ const paidProbe = runMcpPaidCallProbe({ env: actionEnv });
1021
+ if (paidProbe.ok) {
1022
+ summary.firstPaidCall = { ok: true };
1023
+ } else {
1024
+ summary.firstPaidCall = {
1025
+ ok: false,
1026
+ exitCode: paidProbe.exitCode,
1027
+ error: paidProbe.stderr || "paid call returned tool error"
1028
+ };
1029
+ summary.warnings.push("first paid call probe failed");
1030
+ }
1031
+
1032
+ return summary;
1033
+ }
1034
+
868
1035
  function resolveByoWalletEnv({ walletProvider, walletEnvRows, runtimeEnv }) {
869
1036
  const env = {};
870
1037
  for (const row of walletEnvRows ?? []) env[row.key] = row.value;
@@ -1028,16 +1195,21 @@ async function resolveRuntimeConfig({
1028
1195
  runtimeEnv,
1029
1196
  stdin = process.stdin,
1030
1197
  stdout = process.stdout,
1031
- detectInstalledHostsImpl = detectInstalledHosts
1198
+ detectInstalledHostsImpl = detectInstalledHosts,
1199
+ fetchImpl = fetch,
1200
+ runLoginImpl = runLogin,
1201
+ readSavedSessionImpl = readSavedSession
1032
1202
  }) {
1033
1203
  const sessionFile = String(args.sessionFile ?? runtimeEnv.SETTLD_SESSION_FILE ?? defaultSessionPath()).trim();
1034
- const savedSession = await readSavedSession({ sessionPath: sessionFile });
1204
+ const savedSession = await readSavedSessionImpl({ sessionPath: sessionFile });
1035
1205
  const installedHosts = detectInstalledHostsImpl();
1036
1206
  const defaultHost = selectDefaultHost({
1037
1207
  explicitHost: args.host ? String(args.host).toLowerCase() : "",
1038
1208
  installedHosts
1039
1209
  });
1040
1210
  const out = {
1211
+ setupMode: args.setupMode ?? "quick",
1212
+ guidedNext: args.guidedNext,
1041
1213
  host: args.host ?? defaultHost,
1042
1214
  walletMode: args.walletMode,
1043
1215
  baseUrl: String(args.baseUrl ?? runtimeEnv.SETTLD_BASE_URL ?? "").trim(),
@@ -1071,6 +1243,9 @@ async function resolveRuntimeConfig({
1071
1243
  }
1072
1244
 
1073
1245
  if (args.nonInteractive) {
1246
+ if (!out.setupMode) out.setupMode = "quick";
1247
+ if (!SETUP_MODES.has(out.setupMode)) throw new Error("--quick or --advanced are the supported setup modes");
1248
+ if (out.guidedNext === null || out.guidedNext === undefined) out.guidedNext = false;
1074
1249
  if (!SUPPORTED_HOSTS.includes(out.host)) throw new Error(`--host must be one of: ${SUPPORTED_HOSTS.join(", ")}`);
1075
1250
  if (!out.baseUrl) throw new Error("--base-url is required");
1076
1251
  if (!out.tenantId) throw new Error("--tenant-id is required");
@@ -1105,6 +1280,24 @@ async function resolveRuntimeConfig({
1105
1280
  }
1106
1281
  stdout.write("\n");
1107
1282
 
1283
+ if (!out.setupMode || !SETUP_MODES.has(out.setupMode)) {
1284
+ out.setupMode = "quick";
1285
+ }
1286
+ out.setupMode = await promptSelect(
1287
+ rl,
1288
+ stdin,
1289
+ stdout,
1290
+ "Setup mode",
1291
+ [
1292
+ { value: "quick", label: "quick", hint: "Recommended: minimal prompts and guided next steps" },
1293
+ { value: "advanced", label: "advanced", hint: "Full control over all setup options" }
1294
+ ],
1295
+ { defaultValue: out.setupMode, color }
1296
+ );
1297
+ if (out.guidedNext === null || out.guidedNext === undefined) {
1298
+ out.guidedNext = out.setupMode === "quick";
1299
+ }
1300
+
1108
1301
  const hostPromptDefault = out.host && SUPPORTED_HOSTS.includes(out.host) ? out.host : defaultHost;
1109
1302
  const hostOptions = SUPPORTED_HOSTS.map((host) => ({
1110
1303
  value: host,
@@ -1133,68 +1326,105 @@ async function resolveRuntimeConfig({
1133
1326
  { defaultValue: out.walletMode, color }
1134
1327
  );
1135
1328
 
1136
- if (!out.baseUrl) {
1137
- out.baseUrl = await promptLine(rl, "Settld base URL", { defaultValue: "https://api.settld.work" });
1138
- }
1139
- if (!out.tenantId) {
1140
- out.tenantId = await promptLine(rl, "Tenant ID", { defaultValue: "tenant_default" });
1141
- }
1329
+ if (!out.baseUrl) out.baseUrl = DEFAULT_PUBLIC_BASE_URL;
1142
1330
  if (!out.settldApiKey) {
1143
- const canUseSavedSession =
1144
- Boolean(out.sessionCookie) &&
1145
- (!savedSession ||
1146
- (normalizeHttpUrl(out.baseUrl) === normalizeHttpUrl(savedSession?.baseUrl) &&
1147
- String(out.tenantId ?? "").trim() === String(savedSession?.tenantId ?? "").trim()));
1148
- const keyOptions = [];
1149
- if (canUseSavedSession) {
1150
- keyOptions.push({
1151
- value: "session",
1152
- label: "Use saved login session",
1153
- hint: `Reuse ${out.sessionFile} to mint runtime key`
1154
- });
1155
- }
1156
- keyOptions.push(
1157
- { value: "bootstrap", label: "Generate during setup", hint: "Use onboarding bootstrap API key" },
1158
- { value: "manual", label: "Paste existing key", hint: "Use an existing tenant API key" }
1159
- );
1160
- const keyMode = await promptSelect(
1161
- rl,
1162
- stdin,
1163
- stdout,
1164
- "How should setup get your Settld API key?",
1165
- keyOptions,
1166
- { defaultValue: canUseSavedSession ? "session" : "bootstrap", color }
1167
- );
1168
- if (keyMode === "bootstrap") {
1169
- if (!out.bootstrapApiKey) {
1170
- out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1331
+ while (!out.settldApiKey) {
1332
+ const canUseSavedSession =
1333
+ Boolean(out.sessionCookie) &&
1334
+ (!savedSession ||
1335
+ (normalizeHttpUrl(out.baseUrl) === normalizeHttpUrl(savedSession?.baseUrl) &&
1336
+ (!out.tenantId || String(out.tenantId ?? "").trim() === String(savedSession?.tenantId ?? "").trim())));
1337
+ const keyOptions = [];
1338
+ if (canUseSavedSession) {
1339
+ keyOptions.push({
1340
+ value: "session",
1341
+ label: "Use saved login session",
1342
+ hint: `Reuse ${out.sessionFile} to mint runtime key`
1343
+ });
1344
+ } else {
1345
+ keyOptions.push({
1346
+ value: "login",
1347
+ label: "Login / create tenant (recommended)",
1348
+ hint: "OTP sign-in; no API key required"
1349
+ });
1171
1350
  }
1172
- if (!out.bootstrapKeyId) {
1173
- out.bootstrapKeyId = await promptLine(rl, "Generated key ID (optional)", { required: false });
1351
+ keyOptions.push(
1352
+ { value: "bootstrap", label: "Generate during setup", hint: "Use onboarding bootstrap API key" },
1353
+ { value: "manual", label: "Paste existing key", hint: "Use an existing tenant API key" }
1354
+ );
1355
+ const keyMode = await promptSelect(
1356
+ rl,
1357
+ stdin,
1358
+ stdout,
1359
+ "How should setup get your Settld API key?",
1360
+ keyOptions,
1361
+ { defaultValue: canUseSavedSession ? "session" : "login", color }
1362
+ );
1363
+ if (keyMode === "login") {
1364
+ try {
1365
+ await runLoginImpl({
1366
+ argv: ["--base-url", out.baseUrl, "--session-file", out.sessionFile],
1367
+ stdin,
1368
+ stdout,
1369
+ fetchImpl
1370
+ });
1371
+ const refreshedSession = await readSavedSessionImpl({ sessionPath: out.sessionFile });
1372
+ if (!refreshedSession) throw new Error("login did not produce a saved session");
1373
+ out.baseUrl = String(refreshedSession.baseUrl ?? out.baseUrl).trim() || out.baseUrl;
1374
+ out.tenantId = String(refreshedSession.tenantId ?? out.tenantId).trim();
1375
+ out.sessionCookie = String(refreshedSession.cookie ?? out.sessionCookie).trim();
1376
+ if (savedSession) {
1377
+ savedSession.baseUrl = refreshedSession.baseUrl;
1378
+ savedSession.tenantId = refreshedSession.tenantId;
1379
+ savedSession.cookie = refreshedSession.cookie;
1380
+ }
1381
+ } catch (err) {
1382
+ stdout.write(`Login failed: ${err?.message ?? "unknown error"}\n`);
1383
+ stdout.write("Choose `Generate during setup` if your deployment does not expose public signup/login.\n");
1384
+ }
1385
+ continue;
1174
1386
  }
1175
- if (!out.bootstrapScopes) {
1176
- out.bootstrapScopes = await promptLine(rl, "Generated key scopes CSV (optional)", { required: false });
1387
+ if (keyMode === "bootstrap") {
1388
+ if (!out.bootstrapApiKey) {
1389
+ out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1390
+ }
1391
+ if (!out.bootstrapKeyId) {
1392
+ out.bootstrapKeyId = await promptLine(rl, "Generated key ID (optional)", { required: false });
1393
+ }
1394
+ if (!out.bootstrapScopes) {
1395
+ out.bootstrapScopes = await promptLine(rl, "Generated key scopes CSV (optional)", { required: false });
1396
+ }
1397
+ break;
1398
+ }
1399
+ if (keyMode === "manual") {
1400
+ out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1401
+ break;
1177
1402
  }
1178
- } else if (keyMode === "manual") {
1179
- out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1180
- } else {
1181
1403
  out.bootstrapApiKey = "";
1404
+ break;
1182
1405
  }
1183
1406
  }
1407
+ if (!out.tenantId) {
1408
+ out.tenantId = await promptLine(rl, "Tenant ID", { defaultValue: "tenant_default" });
1409
+ }
1184
1410
 
1185
1411
  if (out.walletMode === "managed") {
1186
- out.walletBootstrap = await promptSelect(
1187
- rl,
1188
- stdin,
1189
- stdout,
1190
- "Managed wallet bootstrap",
1191
- [
1192
- { value: "auto", label: "auto", hint: "Use local Circle key when present, else remote bootstrap" },
1193
- { value: "local", label: "local", hint: "Always use local Circle API key flow" },
1194
- { value: "remote", label: "remote", hint: "Always use tenant onboarding endpoint" }
1195
- ],
1196
- { defaultValue: out.walletBootstrap || "auto", color }
1197
- );
1412
+ if (out.setupMode === "quick") {
1413
+ out.walletBootstrap = out.walletBootstrap || "auto";
1414
+ } else {
1415
+ out.walletBootstrap = await promptSelect(
1416
+ rl,
1417
+ stdin,
1418
+ stdout,
1419
+ "Managed wallet bootstrap",
1420
+ [
1421
+ { value: "auto", label: "auto", hint: "Use local Circle key when present, else remote bootstrap" },
1422
+ { value: "local", label: "local", hint: "Always use local Circle API key flow" },
1423
+ { value: "remote", label: "remote", hint: "Always use tenant onboarding endpoint" }
1424
+ ],
1425
+ { defaultValue: out.walletBootstrap || "auto", color }
1426
+ );
1427
+ }
1198
1428
  if (out.walletBootstrap === "local" && !out.circleApiKey) {
1199
1429
  out.circleApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Circle API key");
1200
1430
  }
@@ -1220,6 +1450,9 @@ async function resolveRuntimeConfig({
1220
1450
  out.smoke = false;
1221
1451
  out.skipProfileApply = true;
1222
1452
  out.dryRun = true;
1453
+ out.guidedNext = false;
1454
+ } else if (out.setupMode === "quick") {
1455
+ out.profileId = out.profileId || "engineering-spend";
1223
1456
  } else {
1224
1457
  out.preflight = await promptBooleanChoice(
1225
1458
  rl,
@@ -1296,7 +1529,10 @@ export async function runOnboard({
1296
1529
  requestRuntimeBootstrapMcpEnvImpl = requestRuntimeBootstrapMcpEnv,
1297
1530
  requestRemoteWalletBootstrapImpl = requestRemoteWalletBootstrap,
1298
1531
  runPreflightChecksImpl = runPreflightChecks,
1299
- detectInstalledHostsImpl = detectInstalledHosts
1532
+ detectInstalledHostsImpl = detectInstalledHosts,
1533
+ runLoginImpl = runLogin,
1534
+ readSavedSessionImpl = readSavedSession,
1535
+ runWalletCliImpl = runWalletCli
1300
1536
  } = {}) {
1301
1537
  const args = parseArgs(argv);
1302
1538
  if (args.help) {
@@ -1305,7 +1541,7 @@ export async function runOnboard({
1305
1541
  }
1306
1542
 
1307
1543
  const showSteps = args.format !== "json";
1308
- const totalSteps = args.preflightOnly ? 4 : 5;
1544
+ const totalSteps = args.preflightOnly ? 4 : 6;
1309
1545
  let step = 1;
1310
1546
 
1311
1547
  if (showSteps) printStep(stdout, step, totalSteps, "Resolve setup configuration");
@@ -1314,7 +1550,10 @@ export async function runOnboard({
1314
1550
  runtimeEnv,
1315
1551
  stdin,
1316
1552
  stdout,
1317
- detectInstalledHostsImpl
1553
+ detectInstalledHostsImpl,
1554
+ fetchImpl,
1555
+ runLoginImpl,
1556
+ readSavedSessionImpl
1318
1557
  });
1319
1558
  step += 1;
1320
1559
  const normalizedBaseUrl = normalizeHttpUrl(mustString(config.baseUrl, "SETTLD_BASE_URL / --base-url"));
@@ -1512,9 +1751,27 @@ export async function runOnboard({
1512
1751
  await fs.writeFile(args.outEnv, toEnvFileText(mergedEnv), "utf8");
1513
1752
  }
1514
1753
 
1754
+ if (showSteps) printStep(stdout, step, totalSteps, "Guided next steps");
1755
+ const guided = await runGuidedQuickFlow({
1756
+ enabled: Boolean(config.setupMode === "quick" && config.guidedNext),
1757
+ walletMode: config.walletMode,
1758
+ normalizedBaseUrl,
1759
+ tenantId,
1760
+ sessionFile: config.sessionFile,
1761
+ sessionCookie: config.sessionCookie,
1762
+ mergedEnv,
1763
+ runtimeEnv,
1764
+ stdin,
1765
+ stdout,
1766
+ runWalletCliImpl
1767
+ });
1768
+ step += 1;
1769
+
1515
1770
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize output");
1516
1771
  const payload = {
1517
1772
  ok: true,
1773
+ setupMode: config.setupMode,
1774
+ guided,
1518
1775
  host: config.host,
1519
1776
  wallet: {
1520
1777
  mode: config.walletMode,
@@ -1546,6 +1803,7 @@ export async function runOnboard({
1546
1803
  lines.push("Settld onboard complete.");
1547
1804
  lines.push(`Host: ${config.host}`);
1548
1805
  lines.push(`Settld: ${normalizedBaseUrl} (tenant=${tenantId})`);
1806
+ lines.push(`Setup mode: ${config.setupMode}`);
1549
1807
  lines.push(`Preflight: ${config.preflight ? "passed" : "skipped"}`);
1550
1808
  lines.push(`Wallet mode: ${config.walletMode}`);
1551
1809
  lines.push(`Wallet bootstrap mode: ${walletBootstrapMode}`);
@@ -1553,6 +1811,18 @@ export async function runOnboard({
1553
1811
  if (wallet?.wallets?.escrow?.walletId) lines.push(`Escrow wallet: ${wallet.wallets.escrow.walletId}`);
1554
1812
  if (wallet?.tokenIdUsdc) lines.push(`USDC token id: ${wallet.tokenIdUsdc}`);
1555
1813
  if (args.outEnv) lines.push(`Wrote env file: ${args.outEnv}`);
1814
+ if (guided?.enabled) {
1815
+ lines.push("");
1816
+ lines.push("Guided quick flow:");
1817
+ if (guided.ran) {
1818
+ lines.push(`- wallet fund: ${guided.walletFund?.ok ? "ok" : "not completed"}`);
1819
+ lines.push(`- wallet balance watch: ${guided.walletBalanceWatch?.ok ? "ok" : "not completed"}`);
1820
+ lines.push(`- first paid call: ${guided.firstPaidCall?.ok ? "ok" : "failed"}`);
1821
+ } else {
1822
+ lines.push("- skipped");
1823
+ }
1824
+ for (const warning of guided.warnings ?? []) lines.push(`- warning: ${warning}`);
1825
+ }
1556
1826
  lines.push("");
1557
1827
  lines.push("Combined exports:");
1558
1828
  lines.push(toExportText(mergedEnv));