@totalreclaw/totalreclaw 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/index.ts +451 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,9 +79,9 @@ Most of the time you won't use these directly -- the automatic hooks handle memo
|
|
|
79
79
|
| Tier | Memories | Reads | Storage | Price |
|
|
80
80
|
|------|----------|-------|---------|-------|
|
|
81
81
|
| **Free** | 500/month | Unlimited | Testnet (trial) | $0 |
|
|
82
|
-
| **Pro** | Unlimited | Unlimited | Permanent on-chain (Gnosis) |
|
|
82
|
+
| **Pro** | Unlimited | Unlimited | Permanent on-chain (Gnosis) | See `totalreclaw_status` |
|
|
83
83
|
|
|
84
|
-
Pay with card via Stripe. Counter resets monthly.
|
|
84
|
+
Pay with card via Stripe. Use `totalreclaw_status` to check current pricing. Counter resets monthly.
|
|
85
85
|
|
|
86
86
|
## Using with Other Agents
|
|
87
87
|
|
package/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - totalreclaw_status -- check billing/subscription status
|
|
10
10
|
* - totalreclaw_consolidate -- scan and merge near-duplicate memories
|
|
11
11
|
* - totalreclaw_import_from -- import memories from other tools (Mem0, MCP Memory, etc.)
|
|
12
|
+
* - totalreclaw_upgrade -- create Stripe checkout for Pro upgrade
|
|
13
|
+
* - totalreclaw_migrate -- migrate testnet memories to mainnet after Pro upgrade
|
|
12
14
|
*
|
|
13
15
|
* Also registers a `before_agent_start` hook that automatically injects
|
|
14
16
|
* relevant memories into the agent's context.
|
|
@@ -134,6 +136,9 @@ const MAX_FACTS_PER_EXTRACTION = 15;
|
|
|
134
136
|
// Store-time near-duplicate detection (consolidation module)
|
|
135
137
|
const STORE_DEDUP_ENABLED = process.env.TOTALRECLAW_STORE_DEDUP !== 'false';
|
|
136
138
|
|
|
139
|
+
// One-time welcome-back message for returning Pro users (set during init, consumed by first before_agent_start)
|
|
140
|
+
let welcomeBackMessage: string | null = null;
|
|
141
|
+
|
|
137
142
|
// B2: Minimum relevance threshold — cosine below this means no memory injection
|
|
138
143
|
const RELEVANCE_THRESHOLD = parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3');
|
|
139
144
|
|
|
@@ -421,6 +426,45 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
|
421
426
|
subgraphOwner = userId;
|
|
422
427
|
}
|
|
423
428
|
}
|
|
429
|
+
|
|
430
|
+
// One-time billing check for returning users (imported recovery phrase).
|
|
431
|
+
// If they already have an active Pro subscription, inform them on next conversation start.
|
|
432
|
+
if (existingUserId && authKeyHex) {
|
|
433
|
+
try {
|
|
434
|
+
const walletAddr = subgraphOwner || userId || '';
|
|
435
|
+
if (walletAddr) {
|
|
436
|
+
const billingUrl = (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, '');
|
|
437
|
+
const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
|
|
438
|
+
method: 'GET',
|
|
439
|
+
headers: {
|
|
440
|
+
'Authorization': `Bearer ${authKeyHex}`,
|
|
441
|
+
'Accept': 'application/json',
|
|
442
|
+
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
if (resp.ok) {
|
|
446
|
+
const billingData = await resp.json() as Record<string, unknown>;
|
|
447
|
+
const tier = billingData.tier as string;
|
|
448
|
+
const expiresAt = billingData.expires_at as string | undefined;
|
|
449
|
+
// Populate billing cache for future use.
|
|
450
|
+
writeBillingCache({
|
|
451
|
+
tier: tier || 'free',
|
|
452
|
+
free_writes_used: (billingData.free_writes_used as number) ?? 0,
|
|
453
|
+
free_writes_limit: (billingData.free_writes_limit as number) ?? 0,
|
|
454
|
+
features: billingData.features as BillingCache['features'] | undefined,
|
|
455
|
+
checked_at: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
if (tier === 'pro' && expiresAt) {
|
|
458
|
+
const expiryDate = new Date(expiresAt).toLocaleDateString();
|
|
459
|
+
welcomeBackMessage = `Welcome back! Your Pro subscription is active (expires: ${expiryDate}).`;
|
|
460
|
+
logger.info(`Returning Pro user detected — expires ${expiryDate}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// Best-effort — don't block initialization on billing check failure.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
424
468
|
}
|
|
425
469
|
|
|
426
470
|
function isDocker(): boolean {
|
|
@@ -682,6 +726,138 @@ function decryptFromHex(hexBlob: string, key: Buffer): string {
|
|
|
682
726
|
return decrypt(b64, key);
|
|
683
727
|
}
|
|
684
728
|
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
// Migration GraphQL helpers
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
|
|
733
|
+
interface MigrationFact {
|
|
734
|
+
id: string;
|
|
735
|
+
owner: string;
|
|
736
|
+
encryptedBlob: string;
|
|
737
|
+
encryptedEmbedding: string | null;
|
|
738
|
+
decayScore: string;
|
|
739
|
+
isActive: boolean;
|
|
740
|
+
contentFp: string;
|
|
741
|
+
source: string;
|
|
742
|
+
agentId: string;
|
|
743
|
+
version: number;
|
|
744
|
+
timestamp: string;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const MIGRATION_PAGE_SIZE = 1000;
|
|
748
|
+
|
|
749
|
+
/** Execute a GraphQL query against a subgraph endpoint. Returns null on error. */
|
|
750
|
+
async function migrationGqlQuery<T>(
|
|
751
|
+
endpoint: string,
|
|
752
|
+
query: string,
|
|
753
|
+
variables: Record<string, unknown>,
|
|
754
|
+
authKey?: string,
|
|
755
|
+
): Promise<T | null> {
|
|
756
|
+
try {
|
|
757
|
+
const headers: Record<string, string> = {
|
|
758
|
+
'Content-Type': 'application/json',
|
|
759
|
+
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
760
|
+
};
|
|
761
|
+
if (authKey) headers['Authorization'] = `Bearer ${authKey}`;
|
|
762
|
+
const response = await fetch(endpoint, {
|
|
763
|
+
method: 'POST',
|
|
764
|
+
headers,
|
|
765
|
+
body: JSON.stringify({ query, variables }),
|
|
766
|
+
});
|
|
767
|
+
if (!response.ok) return null;
|
|
768
|
+
const json = await response.json() as { data?: T; errors?: unknown[] };
|
|
769
|
+
return json.data ?? null;
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Fetch all active facts by owner from a subgraph, paginated. */
|
|
776
|
+
async function fetchAllFactsByOwner(
|
|
777
|
+
subgraphUrl: string,
|
|
778
|
+
owner: string,
|
|
779
|
+
authKey: string,
|
|
780
|
+
): Promise<MigrationFact[]> {
|
|
781
|
+
const allFacts: MigrationFact[] = [];
|
|
782
|
+
let lastId = '';
|
|
783
|
+
|
|
784
|
+
while (true) {
|
|
785
|
+
const hasLastId = lastId !== '';
|
|
786
|
+
const query = hasLastId
|
|
787
|
+
? `query($owner:Bytes!,$first:Int!,$lastId:String!){facts(where:{owner:$owner,isActive:true,id_gt:$lastId},first:$first,orderBy:id,orderDirection:asc){id owner encryptedBlob encryptedEmbedding decayScore isActive contentFp source agentId version timestamp}}`
|
|
788
|
+
: `query($owner:Bytes!,$first:Int!){facts(where:{owner:$owner,isActive:true},first:$first,orderBy:id,orderDirection:asc){id owner encryptedBlob encryptedEmbedding decayScore isActive contentFp source agentId version timestamp}}`;
|
|
789
|
+
const vars: Record<string, unknown> = hasLastId
|
|
790
|
+
? { owner, first: MIGRATION_PAGE_SIZE, lastId }
|
|
791
|
+
: { owner, first: MIGRATION_PAGE_SIZE };
|
|
792
|
+
|
|
793
|
+
const data = await migrationGqlQuery<{ facts?: MigrationFact[] }>(subgraphUrl, query, vars, authKey);
|
|
794
|
+
const facts = data?.facts ?? [];
|
|
795
|
+
if (facts.length === 0) break;
|
|
796
|
+
allFacts.push(...facts);
|
|
797
|
+
if (facts.length < MIGRATION_PAGE_SIZE) break;
|
|
798
|
+
lastId = facts[facts.length - 1].id;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return allFacts;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Fetch content fingerprints from a subgraph for idempotency. */
|
|
805
|
+
async function fetchContentFingerprintsByOwner(
|
|
806
|
+
subgraphUrl: string,
|
|
807
|
+
owner: string,
|
|
808
|
+
authKey: string,
|
|
809
|
+
): Promise<Set<string>> {
|
|
810
|
+
const fps = new Set<string>();
|
|
811
|
+
let lastId = '';
|
|
812
|
+
|
|
813
|
+
while (true) {
|
|
814
|
+
const hasLastId = lastId !== '';
|
|
815
|
+
const query = hasLastId
|
|
816
|
+
? `query($owner:Bytes!,$first:Int!,$lastId:String!){facts(where:{owner:$owner,isActive:true,id_gt:$lastId},first:$first,orderBy:id,orderDirection:asc){id contentFp}}`
|
|
817
|
+
: `query($owner:Bytes!,$first:Int!){facts(where:{owner:$owner,isActive:true},first:$first,orderBy:id,orderDirection:asc){id contentFp}}`;
|
|
818
|
+
const vars: Record<string, unknown> = hasLastId
|
|
819
|
+
? { owner, first: MIGRATION_PAGE_SIZE, lastId }
|
|
820
|
+
: { owner, first: MIGRATION_PAGE_SIZE };
|
|
821
|
+
|
|
822
|
+
const data = await migrationGqlQuery<{ facts?: Array<{ id: string; contentFp: string }> }>(subgraphUrl, query, vars, authKey);
|
|
823
|
+
const facts = data?.facts ?? [];
|
|
824
|
+
if (facts.length === 0) break;
|
|
825
|
+
for (const f of facts) {
|
|
826
|
+
if (f.contentFp) fps.add(f.contentFp);
|
|
827
|
+
}
|
|
828
|
+
if (facts.length < MIGRATION_PAGE_SIZE) break;
|
|
829
|
+
lastId = facts[facts.length - 1].id;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return fps;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Fetch blind index hashes for given fact IDs. */
|
|
836
|
+
async function fetchBlindIndicesByFactIds(
|
|
837
|
+
subgraphUrl: string,
|
|
838
|
+
factIds: string[],
|
|
839
|
+
authKey: string,
|
|
840
|
+
): Promise<Map<string, string[]>> {
|
|
841
|
+
const result = new Map<string, string[]>();
|
|
842
|
+
const CHUNK = 50;
|
|
843
|
+
|
|
844
|
+
for (let i = 0; i < factIds.length; i += CHUNK) {
|
|
845
|
+
const chunk = factIds.slice(i, i + CHUNK);
|
|
846
|
+
const query = `query($factIds:[String!]!,$first:Int!){blindIndexes(where:{fact_in:$factIds},first:$first){hash fact{id}}}`;
|
|
847
|
+
const data = await migrationGqlQuery<{
|
|
848
|
+
blindIndexes?: Array<{ hash: string; fact: { id: string } }>;
|
|
849
|
+
}>(subgraphUrl, query, { factIds: chunk, first: 1000 }, authKey);
|
|
850
|
+
|
|
851
|
+
for (const entry of data?.blindIndexes ?? []) {
|
|
852
|
+
const existing = result.get(entry.fact.id) || [];
|
|
853
|
+
existing.push(entry.hash);
|
|
854
|
+
result.set(entry.fact.id, existing);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
|
|
685
861
|
/**
|
|
686
862
|
* Fetch existing memories from the vault to provide dedup context for extraction.
|
|
687
863
|
* Returns a lightweight list of {id, text} pairs for the LLM prompt.
|
|
@@ -2183,6 +2359,268 @@ const plugin = {
|
|
|
2183
2359
|
{ name: 'totalreclaw_import_from' },
|
|
2184
2360
|
);
|
|
2185
2361
|
|
|
2362
|
+
// ---------------------------------------------------------------
|
|
2363
|
+
// Tool: totalreclaw_upgrade
|
|
2364
|
+
// ---------------------------------------------------------------
|
|
2365
|
+
|
|
2366
|
+
api.registerTool(
|
|
2367
|
+
{
|
|
2368
|
+
name: 'totalreclaw_upgrade',
|
|
2369
|
+
label: 'Upgrade to Pro',
|
|
2370
|
+
description:
|
|
2371
|
+
'Upgrade to TotalReclaw Pro for unlimited encrypted memories. ' +
|
|
2372
|
+
'Returns a Stripe checkout URL for the user to complete payment via credit/debit card.',
|
|
2373
|
+
parameters: {
|
|
2374
|
+
type: 'object',
|
|
2375
|
+
properties: {},
|
|
2376
|
+
additionalProperties: false,
|
|
2377
|
+
},
|
|
2378
|
+
async execute() {
|
|
2379
|
+
try {
|
|
2380
|
+
await requireFullSetup(api.logger);
|
|
2381
|
+
|
|
2382
|
+
if (!authKeyHex) {
|
|
2383
|
+
return {
|
|
2384
|
+
content: [{ type: 'text', text: 'Auth credentials are not available. Please initialize first.' }],
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const serverUrl = (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, '');
|
|
2389
|
+
const walletAddr = subgraphOwner || userId || '';
|
|
2390
|
+
|
|
2391
|
+
if (!walletAddr) {
|
|
2392
|
+
return {
|
|
2393
|
+
content: [{ type: 'text', text: 'Wallet address not available. Please ensure the plugin is fully initialized.' }],
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
|
|
2398
|
+
method: 'POST',
|
|
2399
|
+
headers: {
|
|
2400
|
+
'Authorization': `Bearer ${authKeyHex}`,
|
|
2401
|
+
'Content-Type': 'application/json',
|
|
2402
|
+
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
2403
|
+
},
|
|
2404
|
+
body: JSON.stringify({
|
|
2405
|
+
wallet_address: walletAddr,
|
|
2406
|
+
tier: 'pro',
|
|
2407
|
+
}),
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
if (!response.ok) {
|
|
2411
|
+
const body = await response.text().catch(() => '');
|
|
2412
|
+
return {
|
|
2413
|
+
content: [{ type: 'text', text: `Failed to create checkout session (HTTP ${response.status}): ${body || response.statusText}` }],
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const data = await response.json() as { checkout_url?: string };
|
|
2418
|
+
|
|
2419
|
+
if (!data.checkout_url) {
|
|
2420
|
+
return {
|
|
2421
|
+
content: [{ type: 'text', text: 'Failed to create checkout session: no checkout URL returned.' }],
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
return {
|
|
2426
|
+
content: [{ type: 'text', text: `Open this URL to upgrade to Pro: ${data.checkout_url}` }],
|
|
2427
|
+
details: { checkout_url: data.checkout_url },
|
|
2428
|
+
};
|
|
2429
|
+
} catch (err: unknown) {
|
|
2430
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2431
|
+
api.logger.error(`totalreclaw_upgrade failed: ${message}`);
|
|
2432
|
+
return {
|
|
2433
|
+
content: [{ type: 'text', text: `Failed to create checkout session: ${message}` }],
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
},
|
|
2437
|
+
},
|
|
2438
|
+
{ name: 'totalreclaw_upgrade' },
|
|
2439
|
+
);
|
|
2440
|
+
|
|
2441
|
+
// ---------------------------------------------------------------
|
|
2442
|
+
// Tool: totalreclaw_migrate
|
|
2443
|
+
// ---------------------------------------------------------------
|
|
2444
|
+
|
|
2445
|
+
api.registerTool(
|
|
2446
|
+
{
|
|
2447
|
+
name: 'totalreclaw_migrate',
|
|
2448
|
+
label: 'Migrate Testnet to Mainnet',
|
|
2449
|
+
description:
|
|
2450
|
+
'Migrate memories from testnet (Base Sepolia) to mainnet (Gnosis) after upgrading to Pro. ' +
|
|
2451
|
+
'Dry-run by default — set confirm=true to execute. Idempotent: re-running skips already-migrated facts.',
|
|
2452
|
+
parameters: {
|
|
2453
|
+
type: 'object',
|
|
2454
|
+
properties: {
|
|
2455
|
+
confirm: {
|
|
2456
|
+
type: 'boolean',
|
|
2457
|
+
description: 'Set to true to execute the migration. Without it, returns a dry-run preview.',
|
|
2458
|
+
default: false,
|
|
2459
|
+
},
|
|
2460
|
+
},
|
|
2461
|
+
additionalProperties: false,
|
|
2462
|
+
},
|
|
2463
|
+
async execute(_params: { confirm?: boolean }) {
|
|
2464
|
+
try {
|
|
2465
|
+
await requireFullSetup(api.logger);
|
|
2466
|
+
|
|
2467
|
+
if (!authKeyHex || !subgraphOwner) {
|
|
2468
|
+
return {
|
|
2469
|
+
content: [{ type: 'text', text: 'Plugin not fully initialized. Ensure TOTALRECLAW_RECOVERY_PHRASE is set.' }],
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
if (!isSubgraphMode()) {
|
|
2474
|
+
return {
|
|
2475
|
+
content: [{ type: 'text', text: 'Migration is only available with the managed service (subgraph mode).' }],
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
const confirm = _params?.confirm === true;
|
|
2480
|
+
const serverUrl = (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, '');
|
|
2481
|
+
|
|
2482
|
+
// 1. Check billing tier
|
|
2483
|
+
const billingResp = await fetch(
|
|
2484
|
+
`${serverUrl}/v1/billing/status?wallet_address=${encodeURIComponent(subgraphOwner)}`,
|
|
2485
|
+
{
|
|
2486
|
+
method: 'GET',
|
|
2487
|
+
headers: {
|
|
2488
|
+
'Authorization': `Bearer ${authKeyHex}`,
|
|
2489
|
+
'Content-Type': 'application/json',
|
|
2490
|
+
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
2491
|
+
},
|
|
2492
|
+
},
|
|
2493
|
+
);
|
|
2494
|
+
if (!billingResp.ok) {
|
|
2495
|
+
return { content: [{ type: 'text', text: `Failed to check billing tier (HTTP ${billingResp.status}).` }] };
|
|
2496
|
+
}
|
|
2497
|
+
const billingData = await billingResp.json() as { tier: string };
|
|
2498
|
+
if (billingData.tier !== 'pro') {
|
|
2499
|
+
return {
|
|
2500
|
+
content: [{ type: 'text', text: 'Migration requires Pro tier. Use totalreclaw_upgrade to upgrade first.' }],
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// 2. Fetch testnet facts via relay (chain=testnet query param)
|
|
2505
|
+
const testnetSubgraphUrl = `${serverUrl}/v1/subgraph?chain=testnet`;
|
|
2506
|
+
const mainnetSubgraphUrl = `${serverUrl}/v1/subgraph`;
|
|
2507
|
+
|
|
2508
|
+
api.logger.info('Fetching testnet facts...');
|
|
2509
|
+
const testnetFacts = await fetchAllFactsByOwner(testnetSubgraphUrl, subgraphOwner, authKeyHex);
|
|
2510
|
+
|
|
2511
|
+
if (testnetFacts.length === 0) {
|
|
2512
|
+
return {
|
|
2513
|
+
content: [{ type: 'text', text: 'No facts found on testnet. Nothing to migrate.' }],
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// 3. Check mainnet for existing facts (idempotency)
|
|
2518
|
+
api.logger.info('Checking mainnet for existing facts...');
|
|
2519
|
+
const mainnetFps = await fetchContentFingerprintsByOwner(mainnetSubgraphUrl, subgraphOwner, authKeyHex);
|
|
2520
|
+
const factsToMigrate = testnetFacts.filter(f => !f.contentFp || !mainnetFps.has(f.contentFp));
|
|
2521
|
+
const alreadyOnMainnet = testnetFacts.length - factsToMigrate.length;
|
|
2522
|
+
|
|
2523
|
+
// 4. Dry-run
|
|
2524
|
+
if (!confirm) {
|
|
2525
|
+
const msg = factsToMigrate.length === 0
|
|
2526
|
+
? `All ${testnetFacts.length} testnet facts already exist on mainnet. Nothing to migrate.`
|
|
2527
|
+
: `Found ${factsToMigrate.length} facts to migrate from testnet to Gnosis mainnet (${alreadyOnMainnet} already on mainnet). Call with confirm=true to proceed.`;
|
|
2528
|
+
return {
|
|
2529
|
+
content: [{ type: 'text', text: msg }],
|
|
2530
|
+
details: {
|
|
2531
|
+
mode: 'dry_run',
|
|
2532
|
+
testnet_facts: testnetFacts.length,
|
|
2533
|
+
already_on_mainnet: alreadyOnMainnet,
|
|
2534
|
+
to_migrate: factsToMigrate.length,
|
|
2535
|
+
},
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// 5. Execute migration
|
|
2540
|
+
if (factsToMigrate.length === 0) {
|
|
2541
|
+
return {
|
|
2542
|
+
content: [{ type: 'text', text: `All ${testnetFacts.length} testnet facts already exist on mainnet. Nothing to migrate.` }],
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Fetch blind indices
|
|
2547
|
+
api.logger.info(`Fetching blind indices for ${factsToMigrate.length} facts...`);
|
|
2548
|
+
const factIds = factsToMigrate.map(f => f.id);
|
|
2549
|
+
const blindIndicesMap = await fetchBlindIndicesByFactIds(testnetSubgraphUrl, factIds, authKeyHex);
|
|
2550
|
+
|
|
2551
|
+
// Build protobuf payloads
|
|
2552
|
+
const payloads: Buffer[] = [];
|
|
2553
|
+
for (const fact of factsToMigrate) {
|
|
2554
|
+
const blobHex = fact.encryptedBlob.startsWith('0x') ? fact.encryptedBlob.slice(2) : fact.encryptedBlob;
|
|
2555
|
+
const indices = blindIndicesMap.get(fact.id) || [];
|
|
2556
|
+
const factPayload: FactPayload = {
|
|
2557
|
+
id: fact.id,
|
|
2558
|
+
timestamp: new Date().toISOString(),
|
|
2559
|
+
owner: subgraphOwner,
|
|
2560
|
+
encryptedBlob: blobHex,
|
|
2561
|
+
blindIndices: indices,
|
|
2562
|
+
decayScore: parseFloat(fact.decayScore) || 0.5,
|
|
2563
|
+
source: fact.source || 'migration',
|
|
2564
|
+
contentFp: fact.contentFp || '',
|
|
2565
|
+
agentId: fact.agentId || 'openclaw-plugin',
|
|
2566
|
+
encryptedEmbedding: fact.encryptedEmbedding || undefined,
|
|
2567
|
+
};
|
|
2568
|
+
payloads.push(encodeFactProtobuf(factPayload));
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// Batch submit (15 per UserOp)
|
|
2572
|
+
const BATCH_SIZE = 15;
|
|
2573
|
+
const batchConfig = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
|
|
2574
|
+
let migrated = 0;
|
|
2575
|
+
let failedBatches = 0;
|
|
2576
|
+
|
|
2577
|
+
for (let i = 0; i < payloads.length; i += BATCH_SIZE) {
|
|
2578
|
+
const batch = payloads.slice(i, i + BATCH_SIZE);
|
|
2579
|
+
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
2580
|
+
const totalBatches = Math.ceil(payloads.length / BATCH_SIZE);
|
|
2581
|
+
api.logger.info(`Migrating batch ${batchNum}/${totalBatches} (${batch.length} facts)...`);
|
|
2582
|
+
|
|
2583
|
+
try {
|
|
2584
|
+
const result = await submitFactBatchOnChain(batch, batchConfig);
|
|
2585
|
+
if (result.success) {
|
|
2586
|
+
migrated += batch.length;
|
|
2587
|
+
} else {
|
|
2588
|
+
failedBatches++;
|
|
2589
|
+
}
|
|
2590
|
+
} catch (err: unknown) {
|
|
2591
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2592
|
+
api.logger.error(`Migration batch ${batchNum} failed: ${msg}`);
|
|
2593
|
+
failedBatches++;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
const resultMsg = failedBatches === 0
|
|
2598
|
+
? `Successfully migrated ${migrated} memories from testnet to Gnosis mainnet.`
|
|
2599
|
+
: `Migrated ${migrated}/${factsToMigrate.length} memories. ${failedBatches} batch(es) failed — re-run to retry (idempotent).`;
|
|
2600
|
+
|
|
2601
|
+
return {
|
|
2602
|
+
content: [{ type: 'text', text: resultMsg }],
|
|
2603
|
+
details: {
|
|
2604
|
+
mode: 'executed',
|
|
2605
|
+
testnet_facts: testnetFacts.length,
|
|
2606
|
+
already_on_mainnet: alreadyOnMainnet,
|
|
2607
|
+
to_migrate: factsToMigrate.length,
|
|
2608
|
+
migrated,
|
|
2609
|
+
failed_batches: failedBatches,
|
|
2610
|
+
},
|
|
2611
|
+
};
|
|
2612
|
+
} catch (err: unknown) {
|
|
2613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2614
|
+
api.logger.error(`totalreclaw_migrate failed: ${message}`);
|
|
2615
|
+
return {
|
|
2616
|
+
content: [{ type: 'text', text: `Migration failed: ${message}` }],
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
},
|
|
2621
|
+
{ name: 'totalreclaw_migrate' },
|
|
2622
|
+
);
|
|
2623
|
+
|
|
2186
2624
|
// ---------------------------------------------------------------
|
|
2187
2625
|
// Hook: before_agent_start
|
|
2188
2626
|
// ---------------------------------------------------------------
|
|
@@ -2213,6 +2651,13 @@ const plugin = {
|
|
|
2213
2651
|
};
|
|
2214
2652
|
}
|
|
2215
2653
|
|
|
2654
|
+
// One-time welcome-back message for returning Pro users.
|
|
2655
|
+
let welcomeBack = '';
|
|
2656
|
+
if (welcomeBackMessage) {
|
|
2657
|
+
welcomeBack = `\n\n${welcomeBackMessage}`;
|
|
2658
|
+
welcomeBackMessage = null; // Consume — only show once
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2216
2661
|
// Billing cache check — warn if quota is approaching limit.
|
|
2217
2662
|
let billingWarning = '';
|
|
2218
2663
|
try {
|
|
@@ -2286,7 +2731,7 @@ const plugin = {
|
|
|
2286
2731
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
2287
2732
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
2288
2733
|
);
|
|
2289
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
|
|
2734
|
+
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
2290
2735
|
}
|
|
2291
2736
|
}
|
|
2292
2737
|
|
|
@@ -2299,7 +2744,7 @@ const plugin = {
|
|
|
2299
2744
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
2300
2745
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
2301
2746
|
);
|
|
2302
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
|
|
2747
|
+
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
2303
2748
|
}
|
|
2304
2749
|
|
|
2305
2750
|
if (allTrapdoors.length === 0) return undefined;
|
|
@@ -2316,7 +2761,7 @@ const plugin = {
|
|
|
2316
2761
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
2317
2762
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
2318
2763
|
);
|
|
2319
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
|
|
2764
|
+
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
2320
2765
|
}
|
|
2321
2766
|
return undefined;
|
|
2322
2767
|
}
|
|
@@ -2328,7 +2773,7 @@ const plugin = {
|
|
|
2328
2773
|
const lines = cachedFacts.slice(0, 8).map((f, i) =>
|
|
2329
2774
|
`${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
|
|
2330
2775
|
);
|
|
2331
|
-
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
|
|
2776
|
+
return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
2332
2777
|
}
|
|
2333
2778
|
|
|
2334
2779
|
// 5. Decrypt subgraph results and build reranker input.
|
|
@@ -2434,7 +2879,7 @@ const plugin = {
|
|
|
2434
2879
|
});
|
|
2435
2880
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
2436
2881
|
|
|
2437
|
-
return { prependContext: contextString + billingWarning };
|
|
2882
|
+
return { prependContext: contextString + welcomeBack + billingWarning };
|
|
2438
2883
|
}
|
|
2439
2884
|
|
|
2440
2885
|
// --- Server mode (existing behavior) ---
|
|
@@ -2546,7 +2991,7 @@ const plugin = {
|
|
|
2546
2991
|
});
|
|
2547
2992
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
2548
2993
|
|
|
2549
|
-
return { prependContext: contextString + billingWarning };
|
|
2994
|
+
return { prependContext: contextString + welcomeBack + billingWarning };
|
|
2550
2995
|
} catch (err: unknown) {
|
|
2551
2996
|
// The hook must NEVER throw -- log and return undefined.
|
|
2552
2997
|
const message = err instanceof Error ? err.message : String(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "End-to-end encrypted memory for AI agents — portable, yours forever. Automatic extraction, semantic search, and on-chain storage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|