@totalreclaw/totalreclaw 3.3.1-rc.2 → 3.3.1-rc.21
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/CHANGELOG.md +330 -0
- package/SKILL.md +50 -83
- package/api-client.ts +18 -11
- package/config.ts +117 -3
- package/crypto.ts +10 -2
- package/dist/api-client.js +226 -0
- package/dist/billing-cache.js +100 -0
- package/dist/claims-helper.js +606 -0
- package/dist/config.js +280 -0
- package/dist/consolidation.js +258 -0
- package/dist/contradiction-sync.js +1034 -0
- package/dist/crypto.js +138 -0
- package/dist/digest-sync.js +361 -0
- package/dist/download-ux.js +63 -0
- package/dist/embedding.js +86 -0
- package/dist/extractor.js +1225 -0
- package/dist/first-run.js +103 -0
- package/dist/fs-helpers.js +563 -0
- package/dist/gateway-url.js +197 -0
- package/dist/generate-mnemonic.js +13 -0
- package/dist/hot-cache-wrapper.js +101 -0
- package/dist/import-adapters/base-adapter.js +64 -0
- package/dist/import-adapters/chatgpt-adapter.js +238 -0
- package/dist/import-adapters/claude-adapter.js +114 -0
- package/dist/import-adapters/gemini-adapter.js +201 -0
- package/dist/import-adapters/index.js +26 -0
- package/dist/import-adapters/mcp-memory-adapter.js +219 -0
- package/dist/import-adapters/mem0-adapter.js +158 -0
- package/dist/import-adapters/types.js +1 -0
- package/dist/index.js +5348 -0
- package/dist/llm-client.js +686 -0
- package/dist/llm-profile-reader.js +346 -0
- package/dist/lsh.js +62 -0
- package/dist/onboarding-cli.js +750 -0
- package/dist/pair-cli.js +344 -0
- package/dist/pair-crypto.js +359 -0
- package/dist/pair-http.js +404 -0
- package/dist/pair-page.js +826 -0
- package/dist/pair-qr.js +107 -0
- package/dist/pair-remote-client.js +410 -0
- package/dist/pair-session-store.js +566 -0
- package/dist/pin.js +542 -0
- package/dist/qa-bug-report.js +301 -0
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +442 -0
- package/dist/retype-setscope.js +348 -0
- package/dist/semantic-dedup.js +75 -0
- package/dist/subgraph-search.js +289 -0
- package/dist/subgraph-store.js +694 -0
- package/dist/tool-gating.js +58 -0
- package/download-ux.ts +91 -0
- package/embedding.ts +32 -9
- package/fs-helpers.ts +124 -0
- package/gateway-url.ts +57 -9
- package/index.ts +586 -357
- package/llm-client.ts +211 -23
- package/lsh.ts +7 -2
- package/onboarding-cli.ts +114 -1
- package/package.json +19 -5
- package/pair-cli.ts +76 -8
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +28 -17
- package/pair-qr.ts +152 -0
- package/pair-remote-client.ts +540 -0
- package/qa-bug-report.ts +381 -0
- package/relay-headers.ts +50 -0
- package/reranker.ts +73 -0
- package/retype-setscope.ts +12 -0
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +109 -16
package/llm-client.ts
CHANGED
|
@@ -72,8 +72,48 @@ const PROVIDER_KEY_NAMES: Record<string, string[]> = {
|
|
|
72
72
|
cerebras: ['cerebras'],
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* zai has TWO public endpoints. The CODING endpoint is what GLM Coding Plan
|
|
77
|
+
* subscription keys are provisioned against; the STANDARD (PAYG) endpoint
|
|
78
|
+
* serves pay-as-you-go balances. A coding-plan key that hits the STANDARD
|
|
79
|
+
* endpoint returns HTTP 429 with body `"Insufficient balance or no resource
|
|
80
|
+
* package. Please recharge."` — misleading because the subscription is in
|
|
81
|
+
* good standing. Vice-versa for PAYG keys that accidentally hit CODING.
|
|
82
|
+
*
|
|
83
|
+
* 3.3.1-rc.3: exported so the rc.3 auto-fallback (see `chatCompletion`)
|
|
84
|
+
* can flip between them when the upstream error signature matches.
|
|
85
|
+
*/
|
|
86
|
+
export const ZAI_CODING_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
|
87
|
+
export const ZAI_STANDARD_BASE_URL = 'https://api.z.ai/api/paas/v4';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the zai base URL.
|
|
91
|
+
*
|
|
92
|
+
* Precedence:
|
|
93
|
+
* 1. `ZAI_BASE_URL` env var (explicit operator override — read by
|
|
94
|
+
* `CONFIG.zaiBaseUrl` via a getter so tests can mutate the env
|
|
95
|
+
* between calls)
|
|
96
|
+
* 2. Default: coding endpoint (coding-plan-biased; the rc.3 auto-fallback
|
|
97
|
+
* hops to the standard endpoint on an "Insufficient balance" 429).
|
|
98
|
+
*
|
|
99
|
+
* Documented in plugin SKILL.md — Coding-Plan users can leave it unset (or
|
|
100
|
+
* set it explicitly to `https://api.z.ai/api/coding/paas/v4`). PAYG users
|
|
101
|
+
* MUST set it to `https://api.z.ai/api/paas/v4` to avoid the auto-fallback
|
|
102
|
+
* tax on every first call.
|
|
103
|
+
*
|
|
104
|
+
* Scanner-isolation note: the env read lives in `config.ts` (which has no
|
|
105
|
+
* network triggers). This module has network calls, so it cannot touch
|
|
106
|
+
* env vars directly — both rules 1 (env-harvesting) and 2 (potential-
|
|
107
|
+
* exfiltration) in check-scanner.mjs would fire.
|
|
108
|
+
*/
|
|
109
|
+
export function getZaiBaseUrl(): string {
|
|
110
|
+
return CONFIG.zaiBaseUrl;
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
const PROVIDER_BASE_URLS: Record<string, string> = {
|
|
76
|
-
zai:
|
|
114
|
+
// zai: resolved lazily at each init/call so `ZAI_BASE_URL` env changes
|
|
115
|
+
// propagate without a module re-import. See `getZaiBaseUrl()`.
|
|
116
|
+
zai: getZaiBaseUrl(),
|
|
77
117
|
anthropic: 'https://api.anthropic.com/v1',
|
|
78
118
|
openai: 'https://api.openai.com/v1',
|
|
79
119
|
gemini: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
@@ -196,7 +236,13 @@ function buildConfigForProvider(
|
|
|
196
236
|
apiFormatOverride?: 'openai' | 'anthropic';
|
|
197
237
|
} = {},
|
|
198
238
|
): LLMClientConfig | null {
|
|
199
|
-
|
|
239
|
+
// zai's base URL is resolved via `getZaiBaseUrl()` (reads CONFIG) so
|
|
240
|
+
// the `ZAI_BASE_URL` env override takes effect even when this helper is
|
|
241
|
+
// called with no `baseUrlOverride` (i.e. the env-var fallback tier in
|
|
242
|
+
// initLLMClient).
|
|
243
|
+
const defaultForProvider =
|
|
244
|
+
provider === 'zai' ? getZaiBaseUrl() : PROVIDER_BASE_URLS[provider] ?? '';
|
|
245
|
+
const baseUrl = (opts.baseUrlOverride ?? defaultForProvider).replace(/\/+$/, '');
|
|
200
246
|
if (!baseUrl) return null;
|
|
201
247
|
const model =
|
|
202
248
|
opts.modelOverride ??
|
|
@@ -466,7 +512,7 @@ export function resolveLLMConfig(): LLMClientConfig | null {
|
|
|
466
512
|
if (zaiKey) {
|
|
467
513
|
return {
|
|
468
514
|
apiKey: zaiKey,
|
|
469
|
-
baseUrl:
|
|
515
|
+
baseUrl: getZaiBaseUrl(),
|
|
470
516
|
model,
|
|
471
517
|
apiFormat: 'openai',
|
|
472
518
|
};
|
|
@@ -486,22 +532,29 @@ export function resolveLLMConfig(): LLMClientConfig | null {
|
|
|
486
532
|
|
|
487
533
|
/**
|
|
488
534
|
* Options for chatCompletion. `retry` controls the 429 + timeout backoff
|
|
489
|
-
* loop
|
|
490
|
-
*
|
|
535
|
+
* loop. Defaults to 5 attempts with 2s → 4s → 8s → 16s → 32s backoff
|
|
536
|
+
* (total budget ~62s) — rc.1/rc.2 QA showed multi-minute upstream outages
|
|
537
|
+
* that blew through the rc.2 7s budget. Configurable via
|
|
538
|
+
* `TOTALRECLAW_LLM_RETRY_BUDGET_MS` env (cap on cumulative retry-delay).
|
|
491
539
|
*/
|
|
492
540
|
export interface ChatCompletionOptions {
|
|
493
541
|
maxTokens?: number;
|
|
494
542
|
temperature?: number;
|
|
495
543
|
/**
|
|
496
|
-
* Retry behaviour. Defaults
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
544
|
+
* Retry behaviour. Defaults mirror the rc.3 budget: 5 attempts, 2s base
|
|
545
|
+
* delay, exponential. Set `attempts: 0` (or `1`) to disable retry. Pass
|
|
546
|
+
* a `logger` for visibility; without one, retries are silent.
|
|
547
|
+
*
|
|
548
|
+
* `budgetMs` caps the cumulative retry-delay time — after an attempt
|
|
549
|
+
* fails, we compute the next delay and skip it (falling through to the
|
|
550
|
+
* give-up path) if adding it would exceed the budget. Defaults to the
|
|
551
|
+
* value read from `TOTALRECLAW_LLM_RETRY_BUDGET_MS` at module load,
|
|
552
|
+
* which itself defaults to 60_000ms.
|
|
501
553
|
*/
|
|
502
554
|
retry?: {
|
|
503
555
|
attempts?: number;
|
|
504
556
|
baseDelayMs?: number;
|
|
557
|
+
budgetMs?: number;
|
|
505
558
|
};
|
|
506
559
|
logger?: {
|
|
507
560
|
info?: (msg: string) => void;
|
|
@@ -512,17 +565,76 @@ export interface ChatCompletionOptions {
|
|
|
512
565
|
timeoutMs?: number;
|
|
513
566
|
}
|
|
514
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Default retry budget in ms. Configurable via
|
|
570
|
+
* `TOTALRECLAW_LLM_RETRY_BUDGET_MS` env var — read by `config.ts`. Callers
|
|
571
|
+
* can override per-call via `retry.budgetMs`. 60_000ms covers ~8 minutes
|
|
572
|
+
* worth of upstream outages with the 2s→32s schedule.
|
|
573
|
+
*
|
|
574
|
+
* Scanner-isolation note: the env read lives in `config.ts` so this file
|
|
575
|
+
* stays clean of env-harvesting triggers.
|
|
576
|
+
*/
|
|
577
|
+
export const DEFAULT_RETRY_BUDGET_MS: number = CONFIG.llmRetryBudgetMs;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Structured error thrown when the extraction LLM upstream is unreachable
|
|
581
|
+
* after the full retry budget is exhausted. The extraction pipeline
|
|
582
|
+
* recognizes this via `err instanceof LLMUpstreamOutageError` and can
|
|
583
|
+
* choose to:
|
|
584
|
+
* - queue the message batch for retry next turn,
|
|
585
|
+
* - surface a one-time notification to the user, or
|
|
586
|
+
* - simply skip this extraction window silently.
|
|
587
|
+
*/
|
|
588
|
+
export class LLMUpstreamOutageError extends Error {
|
|
589
|
+
readonly attempts: number;
|
|
590
|
+
readonly lastStatus?: number;
|
|
591
|
+
constructor(message: string, attempts: number, lastStatus?: number) {
|
|
592
|
+
super(message);
|
|
593
|
+
this.name = 'LLMUpstreamOutageError';
|
|
594
|
+
this.attempts = attempts;
|
|
595
|
+
this.lastStatus = lastStatus;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Detect the "Insufficient balance" error shape from zai. Matches both
|
|
601
|
+
* the exact production wording ("Insufficient balance or no resource
|
|
602
|
+
* package. Please recharge.") and the short "no resource package" variant
|
|
603
|
+
* we've seen in some historical responses.
|
|
604
|
+
*/
|
|
605
|
+
export function isZaiBalanceError(errorMessage: string): boolean {
|
|
606
|
+
const m = errorMessage.toLowerCase();
|
|
607
|
+
return m.includes('insufficient balance') || m.includes('no resource package');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Identify the "other" zai endpoint when the current one returns a balance
|
|
612
|
+
* error — CODING ↔ STANDARD. Returns `null` when the URL is neither of
|
|
613
|
+
* the two zai endpoints we know about (e.g. a self-hosted proxy), which
|
|
614
|
+
* means the fallback logic stays put.
|
|
615
|
+
*/
|
|
616
|
+
export function zaiFallbackBaseUrl(currentBaseUrl: string): string | null {
|
|
617
|
+
const normalized = currentBaseUrl.replace(/\/+$/, '');
|
|
618
|
+
if (normalized === ZAI_CODING_BASE_URL) return ZAI_STANDARD_BASE_URL;
|
|
619
|
+
if (normalized === ZAI_STANDARD_BASE_URL) return ZAI_CODING_BASE_URL;
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
515
623
|
/**
|
|
516
624
|
* Call the LLM chat completion endpoint.
|
|
517
625
|
*
|
|
518
626
|
* Supports both OpenAI-compatible format and Anthropic Messages API,
|
|
519
627
|
* determined by `config.apiFormat`.
|
|
520
628
|
*
|
|
521
|
-
* 3.3.1-rc.
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
629
|
+
* 3.3.1-rc.3 — lifts the retry budget 5 attempts × (2s/4s/8s/16s/32s), total
|
|
630
|
+
* ~62s. Configurable via `TOTALRECLAW_LLM_RETRY_BUDGET_MS`. Adds zai
|
|
631
|
+
* "Insufficient balance" auto-fallback: when a zai 429 carries the balance
|
|
632
|
+
* error body AND we're on one of the two known zai endpoints, we flip to
|
|
633
|
+
* the OTHER endpoint and retry ONCE (accounted for separately from the
|
|
634
|
+
* normal retry loop). On exhaustion, throws `LLMUpstreamOutageError`.
|
|
635
|
+
*
|
|
636
|
+
* Non-retryable errors (4xx other than 429, network refused, JSON parse)
|
|
637
|
+
* fail fast on the first attempt.
|
|
526
638
|
*
|
|
527
639
|
* @returns The assistant's response content, or null on failure.
|
|
528
640
|
*/
|
|
@@ -533,34 +645,96 @@ export async function chatCompletion(
|
|
|
533
645
|
): Promise<string | null> {
|
|
534
646
|
const maxTokens = options?.maxTokens ?? 2048;
|
|
535
647
|
const temperature = options?.temperature ?? 0; // Deterministic output for dedup (same input → same text → same content fingerprint)
|
|
536
|
-
const attempts = Math.max(1, options?.retry?.attempts ??
|
|
537
|
-
const baseDelayMs = Math.max(100, options?.retry?.baseDelayMs ??
|
|
648
|
+
const attempts = Math.max(1, options?.retry?.attempts ?? 5);
|
|
649
|
+
const baseDelayMs = Math.max(100, options?.retry?.baseDelayMs ?? 2000);
|
|
650
|
+
const budgetMs = Math.max(100, options?.retry?.budgetMs ?? DEFAULT_RETRY_BUDGET_MS);
|
|
538
651
|
const timeoutMs = options?.timeoutMs ?? 30_000;
|
|
539
652
|
const logger = options?.logger;
|
|
540
653
|
|
|
654
|
+
// We mutate `activeConfig.baseUrl` in the zai fallback branch so the
|
|
655
|
+
// retried call hits the other endpoint. Shallow-clone so the caller's
|
|
656
|
+
// config object stays untouched.
|
|
657
|
+
const activeConfig: LLMClientConfig = { ...config };
|
|
658
|
+
|
|
659
|
+
// One-shot flag: we only auto-fallback zai once per chatCompletion call
|
|
660
|
+
// to prevent ping-pong between the two endpoints if both reject.
|
|
661
|
+
let zaiFallbackAttempted = false;
|
|
662
|
+
|
|
541
663
|
const callOnce = (): Promise<string | null> =>
|
|
542
|
-
|
|
543
|
-
? chatCompletionAnthropic(
|
|
544
|
-
: chatCompletionOpenAI(
|
|
664
|
+
activeConfig.apiFormat === 'anthropic'
|
|
665
|
+
? chatCompletionAnthropic(activeConfig, messages, maxTokens, temperature, timeoutMs)
|
|
666
|
+
: chatCompletionOpenAI(activeConfig, messages, maxTokens, temperature, timeoutMs);
|
|
545
667
|
|
|
546
668
|
let lastErr: unknown;
|
|
669
|
+
let cumulativeDelayMs = 0;
|
|
670
|
+
let lastStatus: number | undefined;
|
|
671
|
+
|
|
547
672
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
548
673
|
try {
|
|
549
674
|
return await callOnce();
|
|
550
675
|
} catch (err) {
|
|
551
676
|
lastErr = err;
|
|
552
677
|
const msg = err instanceof Error ? err.message : String(err);
|
|
678
|
+
lastStatus = parseHttpStatus(msg) ?? lastStatus;
|
|
679
|
+
|
|
680
|
+
// ── zai "Insufficient balance" auto-fallback ──
|
|
681
|
+
// Fires BEFORE the normal retry accounting. If the error is a zai
|
|
682
|
+
// balance-shaped 429, flip the baseUrl once and immediately retry —
|
|
683
|
+
// no backoff, no decrement of the attempt count. Keeps the total
|
|
684
|
+
// attempt budget reserved for genuine outages.
|
|
685
|
+
if (!zaiFallbackAttempted && /\b429\b/.test(msg) && isZaiBalanceError(msg)) {
|
|
686
|
+
const fallback = zaiFallbackBaseUrl(activeConfig.baseUrl);
|
|
687
|
+
if (fallback) {
|
|
688
|
+
zaiFallbackAttempted = true;
|
|
689
|
+
const oldUrl = activeConfig.baseUrl;
|
|
690
|
+
activeConfig.baseUrl = fallback;
|
|
691
|
+
logger?.info?.(
|
|
692
|
+
`chatCompletion: zai endpoint auto-fallback: ${oldUrl} → ${fallback} due to "Insufficient balance" response`,
|
|
693
|
+
);
|
|
694
|
+
// Retry immediately — do NOT decrement attempts counter further;
|
|
695
|
+
// this "extra" attempt is the fallback freebie.
|
|
696
|
+
attempt--;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
553
701
|
const retryable = isRetryable(msg);
|
|
554
702
|
const isFinalAttempt = attempt >= attempts;
|
|
555
703
|
if (!retryable || isFinalAttempt) {
|
|
556
704
|
// Fail-fast OR last attempt — rethrow.
|
|
557
|
-
if (attempt > 1) {
|
|
558
|
-
|
|
705
|
+
if (attempt > 1 || !retryable) {
|
|
706
|
+
if (retryable) {
|
|
707
|
+
logger?.warn?.(`chatCompletion: giving up after ${attempt} attempts: ${msg.slice(0, 200)}`);
|
|
708
|
+
}
|
|
709
|
+
// Structured outage error when the retryable error budget is
|
|
710
|
+
// fully exhausted — lets downstream recognize vs bail silently.
|
|
711
|
+
if (retryable) {
|
|
712
|
+
throw new LLMUpstreamOutageError(
|
|
713
|
+
`LLM upstream outage after ${attempt} attempts: ${msg.slice(0, 200)}`,
|
|
714
|
+
attempt,
|
|
715
|
+
lastStatus,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
559
718
|
}
|
|
560
719
|
throw err;
|
|
561
720
|
}
|
|
562
|
-
|
|
721
|
+
|
|
722
|
+
// Compute next delay, but respect the cumulative retry-budget cap.
|
|
563
723
|
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
724
|
+
if (cumulativeDelayMs + delayMs > budgetMs) {
|
|
725
|
+
logger?.warn?.(
|
|
726
|
+
`chatCompletion: retry budget exhausted (${cumulativeDelayMs}ms used + ${delayMs}ms next > ${budgetMs}ms budget); surfacing outage after ${attempt} attempts: ${msg.slice(0, 160)}`,
|
|
727
|
+
);
|
|
728
|
+
throw new LLMUpstreamOutageError(
|
|
729
|
+
`LLM upstream outage (budget ${budgetMs}ms exhausted after ${attempt} attempts): ${msg.slice(0, 200)}`,
|
|
730
|
+
attempt,
|
|
731
|
+
lastStatus,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
cumulativeDelayMs += delayMs;
|
|
735
|
+
|
|
736
|
+
// Log only the FIRST retry at INFO to avoid spamming during long
|
|
737
|
+
// outages; subsequent retries are DEBUG (debounced per outage).
|
|
564
738
|
if (attempt === 1) {
|
|
565
739
|
logger?.info?.(
|
|
566
740
|
`chatCompletion: retrying after transient failure (attempt ${attempt}/${attempts}, wait ${delayMs}ms): ${msg.slice(0, 160)}`,
|
|
@@ -578,6 +752,20 @@ export async function chatCompletion(
|
|
|
578
752
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
579
753
|
}
|
|
580
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Parse the HTTP status code from an error message of the form
|
|
757
|
+
* `"LLM API 429: rate limit"` or `"Anthropic API 503: ..."`. Returns
|
|
758
|
+
* `undefined` when the message doesn't follow that shape (e.g. network
|
|
759
|
+
* refused). Used by `LLMUpstreamOutageError.lastStatus` for downstream
|
|
760
|
+
* classification.
|
|
761
|
+
*/
|
|
762
|
+
function parseHttpStatus(errorMessage: string): number | undefined {
|
|
763
|
+
const m = errorMessage.match(/\b(\d{3})\b/);
|
|
764
|
+
if (!m) return undefined;
|
|
765
|
+
const code = parseInt(m[1], 10);
|
|
766
|
+
return code >= 100 && code < 600 ? code : undefined;
|
|
767
|
+
}
|
|
768
|
+
|
|
581
769
|
/**
|
|
582
770
|
* Which LLM-call errors are worth retrying. Exported for testability.
|
|
583
771
|
*
|
package/lsh.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
* Default parameters: 32 bits per table, 20 tables.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
// Lazy-load WASM
|
|
10
|
+
// Lazy-load WASM via createRequire. The shipped `dist/index.js` is ESM-only
|
|
11
|
+
// (`"type":"module"`) so the bare `require` global is undefined at runtime.
|
|
12
|
+
// See issue #124 for the bug this avoids; matches the pattern in
|
|
13
|
+
// claims-helper / consolidation / digest-sync / pin / retype-setscope.
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
const requireWasm = createRequire(import.meta.url);
|
|
11
16
|
let _WasmLshHasher: typeof import('@totalreclaw/core')['WasmLshHasher'] | null = null;
|
|
12
17
|
function getWasmLshHasher() {
|
|
13
|
-
if (!_WasmLshHasher) _WasmLshHasher =
|
|
18
|
+
if (!_WasmLshHasher) _WasmLshHasher = requireWasm('@totalreclaw/core').WasmLshHasher;
|
|
14
19
|
return _WasmLshHasher!;
|
|
15
20
|
}
|
|
16
21
|
|
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.21",
|
|
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": [
|
|
@@ -31,13 +31,23 @@
|
|
|
31
31
|
"author": "TotalReclaw Team",
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@huggingface/transformers": "^4.0.1",
|
|
34
35
|
"@totalreclaw/client": "^1.2.0",
|
|
35
36
|
"@totalreclaw/core": "^2.1.1",
|
|
36
|
-
"@
|
|
37
|
+
"@types/qrcode": "^1.5.6",
|
|
38
|
+
"@types/ws": "^8.5.12",
|
|
37
39
|
"onnxruntime-node": "^1.24.0",
|
|
38
|
-
"qrcode
|
|
40
|
+
"qrcode": "^1.5.4",
|
|
41
|
+
"qrcode-terminal": "^0.12.0",
|
|
42
|
+
"ws": "^8.18.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.5.0"
|
|
39
46
|
},
|
|
47
|
+
"main": "./dist/index.js",
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
40
49
|
"files": [
|
|
50
|
+
"dist/",
|
|
41
51
|
"*.ts",
|
|
42
52
|
"import-adapters/",
|
|
43
53
|
"!**/*.test.ts",
|
|
@@ -50,13 +60,17 @@
|
|
|
50
60
|
"skill.json"
|
|
51
61
|
],
|
|
52
62
|
"scripts": {
|
|
53
|
-
"
|
|
63
|
+
"build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
|
|
64
|
+
"verify-tarball": "node ../scripts/verify-tarball.mjs",
|
|
65
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.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 && npx tsx import-time-smoke.test.ts && npx tsx recall-relevance-gate.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx json-stdout-cleanliness.test.ts",
|
|
66
|
+
"smoke:dist": "npx tsx dist-esm-smoke.test.ts",
|
|
54
67
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
68
|
+
"prepack": "npm run build",
|
|
55
69
|
"prepublishOnly": "node ../scripts/check-scanner.mjs"
|
|
56
70
|
},
|
|
57
71
|
"openclaw": {
|
|
58
72
|
"extensions": [
|
|
59
|
-
"./index.
|
|
73
|
+
"./dist/index.js"
|
|
60
74
|
]
|
|
61
75
|
}
|
|
62
76
|
}
|