@totalreclaw/totalreclaw 3.1.0 → 3.2.0-rc.1
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/config.ts +4 -0
- package/fs-helpers.ts +146 -0
- package/index.ts +352 -212
- package/onboarding-cli.ts +546 -0
- package/package.json +10 -3
- package/tool-gating.ts +69 -0
package/config.ts
CHANGED
|
@@ -95,6 +95,10 @@ export const CONFIG = {
|
|
|
95
95
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
96
96
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
97
97
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
98
|
+
// 3.2.0 onboarding state file — separate from credentials.json so it
|
|
99
|
+
// never contains secrets. Loaded on every plugin init + on every
|
|
100
|
+
// before_tool_call gate check.
|
|
101
|
+
onboardingStatePath: process.env.TOTALRECLAW_STATE_PATH || path.join(home, '.totalreclaw', 'state.json'),
|
|
98
102
|
|
|
99
103
|
// Chain — chainId is no longer user-configurable. It is auto-detected from
|
|
100
104
|
// the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
|
package/fs-helpers.ts
CHANGED
|
@@ -401,6 +401,10 @@ export function autoBootstrapCredentials(
|
|
|
401
401
|
* missing, unreadable, or un-parseable — caller logs but does not throw,
|
|
402
402
|
* since failing to flip the flag only means the banner might show twice,
|
|
403
403
|
* not data loss.
|
|
404
|
+
*
|
|
405
|
+
* NOTE: retained for back-compat with pre-3.2.0 tests. 3.2.0 removes the
|
|
406
|
+
* prependContext banner entirely, so no production code path calls this
|
|
407
|
+
* helper anymore.
|
|
404
408
|
*/
|
|
405
409
|
export function markFirstRunAnnouncementShown(credentialsPath: string): boolean {
|
|
406
410
|
try {
|
|
@@ -415,3 +419,145 @@ export function markFirstRunAnnouncementShown(credentialsPath: string): boolean
|
|
|
415
419
|
return false;
|
|
416
420
|
}
|
|
417
421
|
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Onboarding state file (3.2.0)
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 3.2.0 onboarding state file — `~/.totalreclaw/state.json` (or the path
|
|
429
|
+
* overridden via `TOTALRECLAW_STATE_PATH`). Separate from `credentials.json`
|
|
430
|
+
* so the state blob never contains the mnemonic — callers can log / stat /
|
|
431
|
+
* copy this file freely.
|
|
432
|
+
*
|
|
433
|
+
* Two states only, per the user's "clean-slate, simplest possible" ratification:
|
|
434
|
+
* - `fresh` → no usable credentials; every memory tool is gated.
|
|
435
|
+
* - `active` → credentials.json has a valid mnemonic; tools unblocked.
|
|
436
|
+
*
|
|
437
|
+
* The design doc's richer state machine (awaiting_onboarding_choice /
|
|
438
|
+
* skipped / active_unacked) was collapsed to these two on user's call.
|
|
439
|
+
* Re-expand only if a UX need emerges.
|
|
440
|
+
*/
|
|
441
|
+
export interface OnboardingState {
|
|
442
|
+
onboardingState: 'fresh' | 'active';
|
|
443
|
+
/** ISO-8601 timestamp credentials.json was first created / confirmed. */
|
|
444
|
+
credentialsCreatedAt?: string;
|
|
445
|
+
/** Which onboarding path produced the active state. */
|
|
446
|
+
createdBy?: 'generate' | 'import';
|
|
447
|
+
/** Schema version — bump when the shape changes. */
|
|
448
|
+
version?: string;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Default fresh state for a machine that has never onboarded. */
|
|
452
|
+
export function defaultFreshState(): OnboardingState {
|
|
453
|
+
return { onboardingState: 'fresh', version: '3.2.0' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Load the state file at `statePath`. Returns `null` on any I/O or parse
|
|
458
|
+
* failure. The caller decides whether to initialise a fresh state or treat
|
|
459
|
+
* the missing file as fresh.
|
|
460
|
+
*/
|
|
461
|
+
export function loadOnboardingState(statePath: string): OnboardingState | null {
|
|
462
|
+
try {
|
|
463
|
+
if (!fs.existsSync(statePath)) return null;
|
|
464
|
+
const raw = fs.readFileSync(statePath, 'utf-8');
|
|
465
|
+
const parsed = JSON.parse(raw) as Partial<OnboardingState>;
|
|
466
|
+
// Validate the one required field. Anything else may be absent.
|
|
467
|
+
if (parsed.onboardingState !== 'fresh' && parsed.onboardingState !== 'active') {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
onboardingState: parsed.onboardingState,
|
|
472
|
+
credentialsCreatedAt: typeof parsed.credentialsCreatedAt === 'string' ? parsed.credentialsCreatedAt : undefined,
|
|
473
|
+
createdBy: parsed.createdBy === 'generate' || parsed.createdBy === 'import' ? parsed.createdBy : undefined,
|
|
474
|
+
version: typeof parsed.version === 'string' ? parsed.version : undefined,
|
|
475
|
+
};
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Write the state file atomically (temp file + rename) with mode 0600.
|
|
483
|
+
* Returns `true` on success, `false` on any I/O error — caller logs but
|
|
484
|
+
* does not throw. Failing to persist state means the plugin will re-derive
|
|
485
|
+
* it from credentials.json on next load, which is safe.
|
|
486
|
+
*
|
|
487
|
+
* Atomicity matters here because the state file is consumed by the
|
|
488
|
+
* before_tool_call gate on every tool call: a half-written file would
|
|
489
|
+
* force-gate real memory operations.
|
|
490
|
+
*/
|
|
491
|
+
export function writeOnboardingState(statePath: string, state: OnboardingState): boolean {
|
|
492
|
+
try {
|
|
493
|
+
const dir = path.dirname(statePath);
|
|
494
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
495
|
+
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
496
|
+
fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
497
|
+
fs.renameSync(tmp, statePath);
|
|
498
|
+
return true;
|
|
499
|
+
} catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Derive the current onboarding state for this process by reading
|
|
506
|
+
* credentials.json. Used on plugin load + after CLI wizard writes.
|
|
507
|
+
*
|
|
508
|
+
* Rule (simplest possible, per user's clean-slate ratification):
|
|
509
|
+
* - credentials.json exists + extractable mnemonic is a non-empty string
|
|
510
|
+
* → `active`.
|
|
511
|
+
* - credentials.json missing OR mnemonic missing/empty/non-string
|
|
512
|
+
* → `fresh`.
|
|
513
|
+
*
|
|
514
|
+
* This is intentionally LAX about BIP-39 checksum validation — the wizard
|
|
515
|
+
* validates on write; at load time we trust the on-disk file. If the
|
|
516
|
+
* mnemonic has been hand-edited to garbage, `initialize()` will fail later
|
|
517
|
+
* at key-derivation time and surface the error via needsSetup.
|
|
518
|
+
*
|
|
519
|
+
* Does NOT require a pre-existing state file; 3.1.0 users (if any) with a
|
|
520
|
+
* valid credentials.json → active silently, no migration code path.
|
|
521
|
+
*/
|
|
522
|
+
export function deriveStateFromCredentials(credentialsPath: string): OnboardingState['onboardingState'] {
|
|
523
|
+
const creds = loadCredentialsJson(credentialsPath);
|
|
524
|
+
const mnemonic = extractBootstrapMnemonic(creds);
|
|
525
|
+
return mnemonic && mnemonic.length > 0 ? 'active' : 'fresh';
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Compute the effective onboarding state at plugin-load time. Reads the
|
|
530
|
+
* persisted state file if it exists AND matches what credentials.json
|
|
531
|
+
* implies; otherwise recomputes and writes a fresh state file.
|
|
532
|
+
*
|
|
533
|
+
* The reason we still persist a state file (rather than deriving every
|
|
534
|
+
* call) is to carry the `createdBy` + `credentialsCreatedAt` fields through
|
|
535
|
+
* process restarts — those are small but useful for diagnostics + future
|
|
536
|
+
* migration paths.
|
|
537
|
+
*
|
|
538
|
+
* Returns the effective state. Does not throw.
|
|
539
|
+
*/
|
|
540
|
+
export function resolveOnboardingState(
|
|
541
|
+
credentialsPath: string,
|
|
542
|
+
statePath: string,
|
|
543
|
+
): OnboardingState {
|
|
544
|
+
const implied = deriveStateFromCredentials(credentialsPath);
|
|
545
|
+
const persisted = loadOnboardingState(statePath);
|
|
546
|
+
|
|
547
|
+
// Happy path: persisted state matches what credentials imply → trust it.
|
|
548
|
+
if (persisted && persisted.onboardingState === implied) {
|
|
549
|
+
return persisted;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Mismatch (or no persisted state): recompute from credentials, persist,
|
|
553
|
+
// and return. Do not overwrite a known `createdBy` if we're just
|
|
554
|
+
// upgrading a stale state file.
|
|
555
|
+
const next: OnboardingState = {
|
|
556
|
+
onboardingState: implied,
|
|
557
|
+
version: '3.2.0',
|
|
558
|
+
credentialsCreatedAt: persisted?.credentialsCreatedAt,
|
|
559
|
+
createdBy: persisted?.createdBy,
|
|
560
|
+
};
|
|
561
|
+
writeOnboardingState(statePath, next);
|
|
562
|
+
return next;
|
|
563
|
+
}
|