@totalreclaw/totalreclaw 3.3.1-rc.18 → 3.3.1-rc.19
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/SKILL.md +6 -6
- package/index.ts +6 -0
- package/onboarding-cli.ts +114 -1
- package/package.json +2 -2
- package/pair-cli.ts +41 -8
package/SKILL.md
CHANGED
|
@@ -30,8 +30,8 @@ ClawHub classifies TotalReclaw as a skill. `openclaw skills install totalreclaw`
|
|
|
30
30
|
|
|
31
31
|
If the user says "install totalreclaw" (or any variant) AND no `totalreclaw_*` tools are in your toolset:
|
|
32
32
|
|
|
33
|
-
1. Run via your shell tool: `openclaw plugins install totalreclaw`. (For RC candidates: `openclaw plugins install @totalreclaw/totalreclaw@rc` — the `rc` dist-tag on npm is the latest candidate.)
|
|
34
|
-
2. Tell the user: *"Plugin installed.
|
|
33
|
+
1. Run via your shell tool: `openclaw plugins install totalreclaw`. (For RC candidates: `openclaw plugins install @totalreclaw/totalreclaw@rc` — the `rc` dist-tag on npm is the latest candidate.) If the shell call fails with `command not found` / ENOENT / "not authorized" / similar (managed-service or shell-restricted environment): tell the user *"Your environment doesn't expose the OpenClaw CLI to me. Please install `totalreclaw` from your service's plugins / skills UI, then reply `done`."* Wait for `done` and skip to step 3.
|
|
34
|
+
2. Tell the user verbatim: *"Plugin installed. Please restart your OpenClaw gateway so the new tools bind. Common patterns: `openclaw gateway restart` (native install), `docker restart <your-container-name>` (Docker self-host — substitute your actual container name; `docker ps` will show it), or use your managed service's restart control if you're on a hosted plan. Once the gateway is back up, reply `done` and I'll continue."* Do NOT invoke any restart command yourself — your shell is running inside the gateway you're restarting (catch-22). Wait for the user's `done` before proceeding.
|
|
35
35
|
3. After restart: verify `totalreclaw_pair` is now in your toolset. If not, tell the user the restart didn't take effect and to retry the restart command. Do not loop the install.
|
|
36
36
|
4. Check `~/.totalreclaw/credentials.json` (via `test -s ~/.totalreclaw/credentials.json`). If it exists, confirm *"TotalReclaw is already set up."* and stop.
|
|
37
37
|
5. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR on your transport" below for the rc.5+ additions).
|
|
@@ -45,8 +45,8 @@ If `totalreclaw_*` tools ARE already in your toolset when the user asks, SKIP th
|
|
|
45
45
|
|
|
46
46
|
Used when the user pastes a verbose prompt describing skill install + plugin install + restart + pair (e.g. *"TotalReclaw is a skill at 'totalreclaw' on ClawHub. Install it, then install the plugin, restart the gateway, and walk me through the QR pairing flow"*) OR when a tool returns `onboarding required`.
|
|
47
47
|
|
|
48
|
-
1. `openclaw plugins install @totalreclaw/totalreclaw` (stable). For RC: `@totalreclaw/totalreclaw@rc`.
|
|
49
|
-
2. Restart gateway so plugin routes + tools bind.
|
|
48
|
+
1. `openclaw plugins install @totalreclaw/totalreclaw` (stable). For RC: `@totalreclaw/totalreclaw@rc`. If the CLI isn't available to your shell (managed service / shell-restricted env): ask the user to install `totalreclaw` from their service's plugins UI and reply `done`.
|
|
49
|
+
2. Restart gateway so plugin routes + tools bind. Tell the user verbatim: *"Please restart your gateway: `openclaw gateway restart` (native), `docker restart <your-container-name>` (Docker self-host — replace with your actual container name), or your managed service's restart control. Reply `done` once it's back up."* Do NOT run the restart yourself — your shell is inside the gateway. Wait for the user's `done`.
|
|
50
50
|
3. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR on your transport" below for the rc.5+ additions).
|
|
51
51
|
4. Relay to user verbatim: *"Open <url> in your browser. Enter your 12-word recovery phrase (or let the browser generate one). Confirm PIN <pin>. I'll wait for you to say done."*
|
|
52
52
|
5. After user says done: verify `~/.totalreclaw/credentials.json` exists and is non-empty. If missing, call `totalreclaw_pair` again (PIN likely expired) and resend.
|
|
@@ -66,7 +66,7 @@ The QR encodes ONLY the pair URL. The 6-digit PIN is a separate out-of-band conf
|
|
|
66
66
|
|
|
67
67
|
## Phrase safety (HARD — never break)
|
|
68
68
|
|
|
69
|
-
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard
|
|
69
|
+
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard` (the bare interactive form), `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup paths are (in order of preference): (1) `totalreclaw_pair` tool; (2) if the tool is missing from your tool list — a known gap for some container-based agents, issue #87 — shell out to EITHER `openclaw totalreclaw pair generate --url-pin-only` (single-line JSON `{v,url,pin,expires_at_ms}`) OR `openclaw totalreclaw onboard --pair-only` (single-line JSON `{v,pair_url,pin,expires_at_ms}`, issue #95 — same pair flow, `pair_url` key naming). Both have zero phrase material on stdout (pair-crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
|
|
70
70
|
|
|
71
71
|
## Usage (post-setup)
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER i
|
|
|
78
78
|
|
|
79
79
|
## Diagnostics
|
|
80
80
|
|
|
81
|
-
- `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw
|
|
81
|
+
- `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw` (or, on a managed service that doesn't expose the CLI, ask the user to confirm via their service's plugins UI). If listed but tools still absent (container-agent gap, issue #87), shell out to `openclaw totalreclaw pair generate --url-pin-only` on the gateway host and hand the user the `url` + `pin` from the JSON payload — OR `openclaw totalreclaw onboard --pair-only` (same flow, returns `pair_url` instead of `url`, issue #95). If your shell doesn't reach the gateway host (managed service), tell the user to install/enable the plugin via their service's plugins UI and restart the gateway via the service's restart control. Do NOT suggest the bare `openclaw totalreclaw onboard` (leaks phrase on stdout — deprecated in rc.18, removed next RC).
|
|
82
82
|
- User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
|
|
83
83
|
- `onboarding required` → credentials missing; redo from the pair step.
|
|
84
84
|
- `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
|
package/index.ts
CHANGED
|
@@ -2902,6 +2902,12 @@ const plugin = {
|
|
|
2902
2902
|
credentialsPath: CREDENTIALS_PATH,
|
|
2903
2903
|
statePath: CONFIG.onboardingStatePath,
|
|
2904
2904
|
logger: api.logger,
|
|
2905
|
+
// 3.3.1-rc.18 — wire the pair flow into onboard so the
|
|
2906
|
+
// `--pair-only` flag (issue #95) can delegate to it without
|
|
2907
|
+
// duplicating session-store / URL-builder logic. Same deps
|
|
2908
|
+
// as the standalone `pair` subcommand.
|
|
2909
|
+
pairSessionsPath: CONFIG.pairSessionsPath,
|
|
2910
|
+
renderPairingUrl: (session) => buildPairingUrl(api, session),
|
|
2905
2911
|
// 3.3.1 — supplied to the non-interactive --json onboard path
|
|
2906
2912
|
// so the emitted payload includes the derived Smart Account
|
|
2907
2913
|
// (scope) address. Uses the chain-id default; Pro-tier
|
package/onboarding-cli.ts
CHANGED
|
@@ -59,6 +59,23 @@ import {
|
|
|
59
59
|
// has one place to grow into later).
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 3.3.1-rc.18 (issue #95) — deprecation warning for the interactive
|
|
64
|
+
* phrase-print branch. Emitted to STDERR (never stdout) so it is visible
|
|
65
|
+
* to humans but does not pollute any pipe consuming the wizard's output.
|
|
66
|
+
*
|
|
67
|
+
* The phrase-print branch will be REMOVED in the next RC after rc.18.
|
|
68
|
+
* Users running on a TTY can still complete the flow in rc.18; agents
|
|
69
|
+
* MUST use `--pair-only` or the `totalreclaw_pair` tool today.
|
|
70
|
+
*/
|
|
71
|
+
export const PHRASE_PRINT_DEPRECATION_WARNING =
|
|
72
|
+
'\nDEPRECATION (issue #95): the interactive `openclaw totalreclaw onboard` flow\n' +
|
|
73
|
+
' prints your recovery phrase to this terminal. This is being removed in the\n' +
|
|
74
|
+
' next release candidate. For agent / scripted invocation, use:\n' +
|
|
75
|
+
' openclaw totalreclaw onboard --pair-only\n' +
|
|
76
|
+
' which emits ONLY {pair_url, pin} JSON and routes the phrase through the\n' +
|
|
77
|
+
' browser flow (never on stdout).\n\n';
|
|
78
|
+
|
|
62
79
|
export const COPY = {
|
|
63
80
|
welcome:
|
|
64
81
|
'\nTotalReclaw — Secure onboarding\n\n' +
|
|
@@ -403,6 +420,11 @@ export async function runOnboardingWizard(deps: WizardDeps): Promise<WizardResul
|
|
|
403
420
|
}
|
|
404
421
|
|
|
405
422
|
if (choice === 'generate') {
|
|
423
|
+
// 3.3.1-rc.18 (issue #95) — deprecation banner on stderr ONLY.
|
|
424
|
+
// The phrase-print branch is scheduled for removal in the RC after
|
|
425
|
+
// rc.18; we keep it functional in rc.18 for back-compat with users
|
|
426
|
+
// running the wizard on a real TTY today.
|
|
427
|
+
io.stderr.write(PHRASE_PRINT_DEPRECATION_WARNING);
|
|
406
428
|
io.stdout.write(COPY.generateWarning);
|
|
407
429
|
io.stdout.write(COPY.importRemoteLimitation);
|
|
408
430
|
const mnemonic = genMnemonic();
|
|
@@ -674,6 +696,22 @@ export async function runNonInteractiveOnboard(
|
|
|
674
696
|
* --emit-phrase Include the plaintext phrase in the JSON payload
|
|
675
697
|
* (NOT recommended — the phrase lives in
|
|
676
698
|
* credentials.json; prefer reading it there).
|
|
699
|
+
*
|
|
700
|
+
* 3.3.1-rc.18 — `onboard` accepts:
|
|
701
|
+
* --pair-only Phrase-safe agent-shell flag (issue #95).
|
|
702
|
+
* Delegates to the pair flow and emits a single
|
|
703
|
+
* line of JSON `{v, pair_url, pin, expires_at_ms}`
|
|
704
|
+
* to stdout. Phrase NEVER touches stdout, stderr,
|
|
705
|
+
* or the logger in this mode. Use this for any
|
|
706
|
+
* agent-driven setup; it is the recommended path
|
|
707
|
+
* when a container-based agent does not have the
|
|
708
|
+
* `totalreclaw_pair` tool injected.
|
|
709
|
+
*
|
|
710
|
+
* Requires `pairSessionsPath` + `renderPairingUrl`
|
|
711
|
+
* to be supplied to `registerOnboardingCli`. If
|
|
712
|
+
* absent, `--pair-only` exits non-zero with a
|
|
713
|
+
* clear message instead of falling through to the
|
|
714
|
+
* phrase-print branch.
|
|
677
715
|
*/
|
|
678
716
|
export function registerOnboardingCli(
|
|
679
717
|
program: import('commander').Command,
|
|
@@ -683,6 +721,10 @@ export function registerOnboardingCli(
|
|
|
683
721
|
logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void };
|
|
684
722
|
/** Caller-supplied helper for scope-address derivation. Optional — when absent, JSON output omits `scope_address`. */
|
|
685
723
|
deriveScopeAddress?: (mnemonic: string) => Promise<string | undefined>;
|
|
724
|
+
/** Caller-supplied path to the pair-session store. Required for `--pair-only`. */
|
|
725
|
+
pairSessionsPath?: string;
|
|
726
|
+
/** Caller-supplied URL renderer for the pair flow. Required for `--pair-only`. */
|
|
727
|
+
renderPairingUrl?: (session: import('./pair-session-store.js').PairSession) => string;
|
|
686
728
|
},
|
|
687
729
|
): void {
|
|
688
730
|
const tr = program
|
|
@@ -690,12 +732,13 @@ export function registerOnboardingCli(
|
|
|
690
732
|
.description('TotalReclaw encrypted memory — secure onboarding + status');
|
|
691
733
|
|
|
692
734
|
tr.command('onboard')
|
|
693
|
-
.description('Interactive onboarding: generate or import a recovery phrase (runs locally, no LLM)')
|
|
735
|
+
.description('Interactive onboarding: generate or import a recovery phrase (runs locally, no LLM). For agent-driven setup prefer --pair-only.')
|
|
694
736
|
.option('--non-interactive', 'Exit non-zero if any input would be prompted for (agent-driven use)')
|
|
695
737
|
.option('--json', 'Emit the result as a structured JSON payload. Only valid with --non-interactive.')
|
|
696
738
|
.option('--mode <mode>', 'generate | restore — skip the menu prompt')
|
|
697
739
|
.option('--phrase <phrase>', 'Recovery phrase for --mode restore. `-` reads from stdin.')
|
|
698
740
|
.option('--emit-phrase', 'Include the plaintext phrase in the JSON payload (not recommended). Default: false.')
|
|
741
|
+
.option('--pair-only', 'Phrase-safe agent-invocation mode (issue #95). Emits ONLY {v,pair_url,pin,expires_at_ms} JSON to stdout via the pair flow. Phrase never touches stdout/stderr/logger. RECOMMENDED for any agent or scripted invocation.')
|
|
699
742
|
.action(async (...actionArgs: unknown[]) => {
|
|
700
743
|
// commander: (options, cmd)
|
|
701
744
|
const cliOpts = (actionArgs[0] ?? {}) as {
|
|
@@ -704,8 +747,70 @@ export function registerOnboardingCli(
|
|
|
704
747
|
mode?: string;
|
|
705
748
|
phrase?: string;
|
|
706
749
|
emitPhrase?: boolean;
|
|
750
|
+
pairOnly?: boolean;
|
|
707
751
|
};
|
|
708
752
|
|
|
753
|
+
// ---------------------------------------------------------------
|
|
754
|
+
// 3.3.1-rc.18 — `--pair-only` (issue #95)
|
|
755
|
+
//
|
|
756
|
+
// Phrase-safe agent-shell flag. Delegates to the pair flow and
|
|
757
|
+
// emits a single line of JSON `{v, pair_url, pin, expires_at_ms}`
|
|
758
|
+
// to stdout. By construction:
|
|
759
|
+
// - The pair flow is x25519-only — pair-crypto.ts does NOT
|
|
760
|
+
// import @scure/bip39 and never touches a recovery phrase.
|
|
761
|
+
// - No interactive prompts, no readline, no @scure/bip39 import
|
|
762
|
+
// in this code path. Phrase never enters stdout/stderr/logger.
|
|
763
|
+
// - Stays silent on status transitions (the runPairCli
|
|
764
|
+
// `pair-only` output mode suppresses banners, spinners, and
|
|
765
|
+
// all human-readable copy).
|
|
766
|
+
//
|
|
767
|
+
// This MUST be the path agents take when they need to set up
|
|
768
|
+
// TotalReclaw via a shell. The interactive phrase-print branch
|
|
769
|
+
// below is deprecated for that use case and emits a warning when
|
|
770
|
+
// the user falls through to it.
|
|
771
|
+
// ---------------------------------------------------------------
|
|
772
|
+
if (cliOpts.pairOnly) {
|
|
773
|
+
if (!opts.pairSessionsPath || !opts.renderPairingUrl) {
|
|
774
|
+
process.stderr.write(
|
|
775
|
+
'--pair-only is unavailable: this OpenClaw build did not wire the pair flow into the onboard CLI. ' +
|
|
776
|
+
'Use `openclaw totalreclaw pair generate --url-pin-only` instead.\n',
|
|
777
|
+
);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
// Resolve mode. --mode restore is incompatible with --pair-only
|
|
781
|
+
// since pair flow's "import" mode runs in the browser, not in
|
|
782
|
+
// the CLI. Default to 'generate' silently.
|
|
783
|
+
const pairMode = cliOpts.mode === 'restore' || cliOpts.mode === 'import' ? 'import' : 'generate';
|
|
784
|
+
|
|
785
|
+
// Lazy import — keeps pair-cli + qrcode-terminal off the
|
|
786
|
+
// onboarding hot path when --pair-only is not used.
|
|
787
|
+
const { runPairCli, defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
|
|
788
|
+
const io = buildDefaultPairCliIo();
|
|
789
|
+
try {
|
|
790
|
+
const outcome = await runPairCli(pairMode, {
|
|
791
|
+
sessionsPath: opts.pairSessionsPath,
|
|
792
|
+
renderPairingUrl: opts.renderPairingUrl,
|
|
793
|
+
renderQr: defaultRenderQr,
|
|
794
|
+
io,
|
|
795
|
+
outputMode: 'pair-only',
|
|
796
|
+
});
|
|
797
|
+
if (outcome.status !== 'completed') {
|
|
798
|
+
process.exit(outcome.status === 'canceled' ? 130 : 1);
|
|
799
|
+
}
|
|
800
|
+
process.exit(0);
|
|
801
|
+
} catch (err) {
|
|
802
|
+
// CRITICAL: this catch MUST NOT include the phrase, the
|
|
803
|
+
// mnemonic, or any user secret in the message. The pair flow
|
|
804
|
+
// does not produce phrase material, so this is structurally
|
|
805
|
+
// safe — but defense-in-depth: emit a fixed error string.
|
|
806
|
+
opts.logger.error(
|
|
807
|
+
`pair-only delegation crashed: ${err instanceof Error ? err.message : String(err)}`,
|
|
808
|
+
);
|
|
809
|
+
process.stderr.write('--pair-only failed (see logs).\n');
|
|
810
|
+
process.exit(2);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
709
814
|
if (cliOpts.nonInteractive) {
|
|
710
815
|
// Non-interactive path — no readline, no prompts.
|
|
711
816
|
const mode: 'generate' | 'restore' | null =
|
|
@@ -744,10 +849,18 @@ export function registerOnboardingCli(
|
|
|
744
849
|
});
|
|
745
850
|
|
|
746
851
|
if (cliOpts.json) {
|
|
852
|
+
// 3.3.1-rc.18 (issue #95) — emit deprecation on stderr when
|
|
853
|
+
// the JSON payload is about to include the plaintext phrase.
|
|
854
|
+
// stderr is intentional: stdout must remain a single
|
|
855
|
+
// machine-parseable JSON line.
|
|
856
|
+
if (cliOpts.emitPhrase && result.ok && result.mnemonic) {
|
|
857
|
+
process.stderr.write(PHRASE_PRINT_DEPRECATION_WARNING);
|
|
858
|
+
}
|
|
747
859
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
748
860
|
} else {
|
|
749
861
|
if (result.ok) {
|
|
750
862
|
if (result.mnemonic) {
|
|
863
|
+
process.stderr.write(PHRASE_PRINT_DEPRECATION_WARNING);
|
|
751
864
|
process.stderr.write(
|
|
752
865
|
'WARNING: --emit-phrase was set. The plaintext recovery phrase was returned.\n' +
|
|
753
866
|
'For agent-driven flows, prefer reading ~/.totalreclaw/credentials.json directly ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.1-rc.
|
|
3
|
+
"version": "3.3.1-rc.19",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"skill.json"
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
|
-
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts",
|
|
57
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts",
|
|
58
58
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
59
59
|
"prepublishOnly": "node ../scripts/check-scanner.mjs"
|
|
60
60
|
},
|
package/pair-cli.ts
CHANGED
|
@@ -92,8 +92,15 @@ export interface PairCliOutcome {
|
|
|
92
92
|
* gap) and must shell out to the CLI. Guarantees zero phrase material
|
|
93
93
|
* on stdout by construction — pair-crypto is x25519-only and the slim
|
|
94
94
|
* payload carries nothing BIP-39-adjacent.
|
|
95
|
+
* - 'pair-only': (3.3.1-rc.18, issue #95) the same surface as 'url-pin',
|
|
96
|
+
* but the URL field is named `pair_url` (matching the spec wording
|
|
97
|
+
* for `openclaw totalreclaw onboard --pair-only`). Used by the
|
|
98
|
+
* onboard CLI's `--pair-only` flag to provide a phrase-safe
|
|
99
|
+
* alternative to the interactive phrase-print path. Emits ONLY
|
|
100
|
+
* `{ v, pair_url, pin, expires_at_ms }`. Same zero-phrase invariant
|
|
101
|
+
* as 'url-pin' — the underlying pair flow does no BIP-39 work.
|
|
95
102
|
*/
|
|
96
|
-
export type PairCliOutputMode = 'human' | 'json' | 'url-pin';
|
|
103
|
+
export type PairCliOutputMode = 'human' | 'json' | 'url-pin' | 'pair-only';
|
|
97
104
|
|
|
98
105
|
/**
|
|
99
106
|
* JSON payload emitted by runPairCli when outputMode === 'json'. Printed
|
|
@@ -121,6 +128,20 @@ export interface PairCliUrlPinPayload {
|
|
|
121
128
|
expires_at_ms: number;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Slim payload for outputMode === 'pair-only'. Same shape as
|
|
133
|
+
* `PairCliUrlPinPayload` but with `pair_url` instead of `url` — the
|
|
134
|
+
* key name matches the spec for `onboard --pair-only` (issue #95).
|
|
135
|
+
* Phrase invariant: zero BIP-39 material on stdout by construction
|
|
136
|
+
* (the pair flow is x25519-only).
|
|
137
|
+
*/
|
|
138
|
+
export interface PairCliPairOnlyPayload {
|
|
139
|
+
v: 1;
|
|
140
|
+
pair_url: string;
|
|
141
|
+
pin: string;
|
|
142
|
+
expires_at_ms: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
// ---------------------------------------------------------------------------
|
|
125
146
|
// Default stdout IO
|
|
126
147
|
// ---------------------------------------------------------------------------
|
|
@@ -232,10 +253,12 @@ export async function runPairCli(
|
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
// 2. Build the URL unconditionally, but only render the QR for modes
|
|
235
|
-
// that actually emit it. url-pin
|
|
236
|
-
// no CPU cost, no qrcode-terminal import, no
|
|
256
|
+
// that actually emit it. url-pin and pair-only modes skip the
|
|
257
|
+
// renderer entirely — no CPU cost, no qrcode-terminal import, no
|
|
258
|
+
// ASCII on stdout.
|
|
237
259
|
const url = deps.renderPairingUrl(session);
|
|
238
|
-
const
|
|
260
|
+
const skipsQr = outputMode === 'url-pin' || outputMode === 'pair-only';
|
|
261
|
+
const qrAscii = skipsQr ? '' : await new Promise<string>((resolve) => {
|
|
239
262
|
// Guard against QR renderers that never fire their callback (shouldn't
|
|
240
263
|
// happen with qrcode-terminal, but defensive): a 10-second timeout
|
|
241
264
|
// returns an empty string so we never hang the pairing flow.
|
|
@@ -261,7 +284,8 @@ export async function runPairCli(
|
|
|
261
284
|
}
|
|
262
285
|
});
|
|
263
286
|
|
|
264
|
-
// 3. Emit the visible surface (JSON/url-pin first — single
|
|
287
|
+
// 3. Emit the visible surface (JSON/url-pin/pair-only first — single
|
|
288
|
+
// line — or human copy).
|
|
265
289
|
if (outputMode === 'url-pin') {
|
|
266
290
|
const payload: PairCliUrlPinPayload = {
|
|
267
291
|
v: 1,
|
|
@@ -270,6 +294,14 @@ export async function runPairCli(
|
|
|
270
294
|
expires_at_ms: session.expiresAtMs,
|
|
271
295
|
};
|
|
272
296
|
stdout.write(JSON.stringify(payload) + '\n');
|
|
297
|
+
} else if (outputMode === 'pair-only') {
|
|
298
|
+
const payload: PairCliPairOnlyPayload = {
|
|
299
|
+
v: 1,
|
|
300
|
+
pair_url: url,
|
|
301
|
+
pin: session.secondaryCode,
|
|
302
|
+
expires_at_ms: session.expiresAtMs,
|
|
303
|
+
};
|
|
304
|
+
stdout.write(JSON.stringify(payload) + '\n');
|
|
273
305
|
} else if (outputMode === 'json') {
|
|
274
306
|
const payload: PairCliJsonPayload = {
|
|
275
307
|
v: 1,
|
|
@@ -304,9 +336,10 @@ export async function runPairCli(
|
|
|
304
336
|
canceled = true;
|
|
305
337
|
});
|
|
306
338
|
|
|
307
|
-
// 5. Poll — status transitions only surface in human mode; json/
|
|
308
|
-
// modes stay silent after the single payload
|
|
309
|
-
// stdout get one JSON line and an exit
|
|
339
|
+
// 5. Poll — status transitions only surface in human mode; json /
|
|
340
|
+
// url-pin / pair-only modes stay silent after the single payload
|
|
341
|
+
// line so agents parsing stdout get one JSON line and an exit
|
|
342
|
+
// code, nothing else.
|
|
310
343
|
const emitStatus = (text: string): void => {
|
|
311
344
|
if (outputMode === 'human') stdout.write(text);
|
|
312
345
|
};
|