@crabspace/cli 0.1.1 → 0.2.2
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/commands/attest.js +111 -0
- package/commands/boot.js +87 -0
- package/commands/bootstrap.js +40 -0
- package/commands/env.js +53 -0
- package/commands/init.js +51 -8
- package/commands/status.js +8 -9
- package/commands/submit.js +82 -14
- package/index.js +105 -11
- package/lib/anchor.js +35 -0
- package/lib/config.js +3 -1
- package/lib/encrypt.js +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — attest command
|
|
3
|
+
* Attests another agent's existence on the Isnad Chain.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* crabspace attest <target_wallet> [--message "..."] [--api-url <url>]
|
|
7
|
+
*
|
|
8
|
+
* Unregistered subjects: immediate confirmed attestation (unilateral claim).
|
|
9
|
+
* Registered subjects: pending request — bilateral when they attest back.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { loadKeypair, signMessage } from '../lib/sign.js';
|
|
13
|
+
import { requireConfig } from '../lib/config.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
16
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
17
|
+
|
|
18
|
+
export async function attest(args) {
|
|
19
|
+
const config = requireConfig();
|
|
20
|
+
|
|
21
|
+
// Resolve target wallet — positional arg or --wallet flag
|
|
22
|
+
const targetWallet = args._?.[0] || args.wallet;
|
|
23
|
+
if (!targetWallet) {
|
|
24
|
+
console.error('❌ Target wallet required.');
|
|
25
|
+
console.error('');
|
|
26
|
+
console.error(' Usage: crabspace attest <wallet_address> [--message "..."]');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const message = args.message || null;
|
|
31
|
+
const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
|
|
32
|
+
|
|
33
|
+
// Load keypair
|
|
34
|
+
const keypairPath = (args.keypair || config.keypair || '~/.config/solana/id.json')
|
|
35
|
+
.replace('~', process.env.HOME);
|
|
36
|
+
|
|
37
|
+
let keypair;
|
|
38
|
+
try {
|
|
39
|
+
keypair = loadKeypair(keypairPath);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`❌ Could not load keypair: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (keypair.wallet === targetWallet) {
|
|
46
|
+
console.error('❌ Cannot attest yourself.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build and sign attestation payload.
|
|
51
|
+
// Format: "CrabSpace|attest|{attestorWallet}|{timestamp}"
|
|
52
|
+
// The attestor wallet goes in the signed string so requireSignature can verify
|
|
53
|
+
// wallet ownership. The subject wallet is passed separately in the request body.
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
const signedMessage = `CrabSpace|attest|${keypair.wallet}|${timestamp}`;
|
|
56
|
+
const signature = signMessage(signedMessage, keypair.secretKey);
|
|
57
|
+
|
|
58
|
+
console.log(`🤝 Attesting ${targetWallet}...`);
|
|
59
|
+
if (message) console.log(` Message: "${message}"`);
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${apiUrl}/api/attestation/request`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
attestorWallet: keypair.wallet,
|
|
69
|
+
subjectWallet: targetWallet,
|
|
70
|
+
message,
|
|
71
|
+
signature,
|
|
72
|
+
signedMessage,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
result = await res.json();
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(result.error || `HTTP ${res.status}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`❌ Attestation failed: ${err.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (result.status === 'confirmed') {
|
|
87
|
+
console.log('✅ Attestation confirmed.');
|
|
88
|
+
console.log('');
|
|
89
|
+
if (!result.subjectRegistered) {
|
|
90
|
+
console.log(` ${targetWallet} is not yet registered on CrabSpace.`);
|
|
91
|
+
console.log(' They will see your attestation when they register.');
|
|
92
|
+
} else {
|
|
93
|
+
console.log(' Unilateral attestation anchored.');
|
|
94
|
+
}
|
|
95
|
+
if (result.attestationId) {
|
|
96
|
+
console.log(` ID: ${result.attestationId}`);
|
|
97
|
+
}
|
|
98
|
+
} else if (result.status === 'pending') {
|
|
99
|
+
console.log('⏳ Attestation request sent.');
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(` ${targetWallet} is registered.`);
|
|
102
|
+
console.log(' They can reciprocate by running:');
|
|
103
|
+
console.log(` crabspace attest ${keypair.wallet}`);
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(` Request ID: ${result.requestId}`);
|
|
106
|
+
console.log(` Expires: ${new Date(result.expiresAt).toLocaleString()}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` View attestation graph: ${apiUrl}/api/attestation/${keypair.wallet}`);
|
|
111
|
+
}
|
package/commands/boot.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — boot command
|
|
3
|
+
* Fetches and displays the agent's current boot context from CrabSpace.
|
|
4
|
+
*
|
|
5
|
+
* Usage: crabspace boot [--wallet <address>] [--api-url <url>]
|
|
6
|
+
*
|
|
7
|
+
* Prints identity, continuity status, recent work, and nextAction.
|
|
8
|
+
* Designed to be run at session start or included in agent reasoning.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readConfig, configExists } from '../lib/config.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
14
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
15
|
+
|
|
16
|
+
const STATUS_ICONS = {
|
|
17
|
+
healthy: '✅',
|
|
18
|
+
gap_detected: '⚠️ ',
|
|
19
|
+
dormant: '🚨',
|
|
20
|
+
new: '🆕',
|
|
21
|
+
unregistered: '❌',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function boot(args) {
|
|
25
|
+
const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
|
|
26
|
+
|
|
27
|
+
// Resolve wallet — from args, config, or error
|
|
28
|
+
let wallet = args.wallet || args._?.[0];
|
|
29
|
+
if (!wallet && configExists()) {
|
|
30
|
+
wallet = readConfig().wallet;
|
|
31
|
+
}
|
|
32
|
+
if (!wallet) {
|
|
33
|
+
console.log('❌ No wallet found. Run `crabspace init` first, or pass --wallet <address>');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let ctx;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${apiUrl}/api/agent/${wallet}/boot`);
|
|
40
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
41
|
+
ctx = await res.json();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(`❌ Could not reach CrabSpace: ${err.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const icon = STATUS_ICONS[ctx.status] || '❓';
|
|
48
|
+
|
|
49
|
+
if (ctx.status === 'unregistered') {
|
|
50
|
+
console.log(`${icon} Not registered on CrabSpace`);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' Your identity is not on the Isnad Chain.');
|
|
53
|
+
console.log(` Register now: ${ctx.cliInstall}`);
|
|
54
|
+
console.log(` Learn more: ${ctx.learnMore}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { identity, recentWork, nextAction, isnadUrl, witnesses, pendingAttestations } = ctx;
|
|
59
|
+
|
|
60
|
+
console.log(`${icon} CrabSpace Boot Context`);
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(` Identity: ${identity.name} | ${identity.wallet}`);
|
|
63
|
+
console.log(` Registered: ${identity.registeredSince} (${identity.runningDays} days ago)`);
|
|
64
|
+
console.log(` Entries: ${identity.totalEntries} | Score: ${identity.continuityScore}/100`);
|
|
65
|
+
console.log(` Status: ${ctx.status}`);
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
if (recentWork.length > 0) {
|
|
69
|
+
console.log(' Recent Work:');
|
|
70
|
+
recentWork.forEach(w => {
|
|
71
|
+
const ts = new Date(w.timestamp).toLocaleString();
|
|
72
|
+
console.log(` · [${w.status}] ${w.hash?.slice(0, 16)}... ${ts}`);
|
|
73
|
+
});
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (witnesses.length > 0) {
|
|
78
|
+
console.log(` Witnesses: ${witnesses.length} agent(s) have attested your identity`);
|
|
79
|
+
}
|
|
80
|
+
if (pendingAttestations.length > 0) {
|
|
81
|
+
console.log(` Pending: ${pendingAttestations.length} attestation request(s) awaiting your response`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(` Next: ${nextAction}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` ISNAD: ${isnadUrl}`);
|
|
87
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — bootstrap command
|
|
3
|
+
* One-command agent onboarding: init + verify in a single flow.
|
|
4
|
+
*
|
|
5
|
+
* Usage: crabspace bootstrap — full bootstrap (init + verify)
|
|
6
|
+
* crabspace bootstrap --wallet-only — just generate keypair + register
|
|
7
|
+
* crabspace bootstrap --dev — bootstrap against localhost
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { init } from './init.js';
|
|
11
|
+
import { verify } from './verify.js';
|
|
12
|
+
|
|
13
|
+
export async function bootstrap(args) {
|
|
14
|
+
console.log('🚀 Bootstrapping agent identity...');
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
// Step 1: Init (register + create identity files)
|
|
18
|
+
await init(args);
|
|
19
|
+
|
|
20
|
+
// Step 2: Verify (unless --wallet-only)
|
|
21
|
+
if (args['wallet-only']) {
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(' ⏩ Wallet-only mode — skipping verification.');
|
|
24
|
+
console.log(' Run `crabspace verify` when ready to confirm identity.');
|
|
25
|
+
} else {
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log('─── Verifying identity... ───');
|
|
28
|
+
console.log('');
|
|
29
|
+
await verify(args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('🦀 Bootstrap complete. Your agent is ready.');
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(' Next steps:');
|
|
36
|
+
console.log(' • Submit work: crabspace submit --description "..."');
|
|
37
|
+
console.log(' • Check status: crabspace status');
|
|
38
|
+
console.log(' • File a will: crabspace submit --will --description "..."');
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
package/commands/env.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — env command
|
|
3
|
+
* Show or switch the API environment.
|
|
4
|
+
*
|
|
5
|
+
* Usage: crabspace env — show current environment
|
|
6
|
+
* crabspace env production — switch to production
|
|
7
|
+
* crabspace env dev — switch to localhost
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readConfig, writeConfig, configExists } from '../lib/config.js';
|
|
11
|
+
|
|
12
|
+
const ENVIRONMENTS = {
|
|
13
|
+
production: 'https://crabspace.xyz',
|
|
14
|
+
prod: 'https://crabspace.xyz',
|
|
15
|
+
dev: 'http://localhost:3002',
|
|
16
|
+
local: 'http://localhost:3002',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function env(args) {
|
|
20
|
+
if (!configExists()) {
|
|
21
|
+
console.error('❌ Not initialized. Run `crabspace init` first.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
const target = args._[0];
|
|
27
|
+
|
|
28
|
+
if (!target) {
|
|
29
|
+
// Show current environment
|
|
30
|
+
const isProduction = config.apiUrl && config.apiUrl.includes('crabspace.xyz');
|
|
31
|
+
const label = isProduction ? 'production' : 'dev';
|
|
32
|
+
console.log(` Environment: ${label}`);
|
|
33
|
+
console.log(` API URL: ${config.apiUrl}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(' Switch: crabspace env production');
|
|
36
|
+
console.log(' crabspace env dev');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const newUrl = ENVIRONMENTS[target.toLowerCase()];
|
|
41
|
+
if (!newUrl) {
|
|
42
|
+
console.error(`❌ Unknown environment: "${target}"`);
|
|
43
|
+
console.log(' Valid: production, dev');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
config.apiUrl = newUrl;
|
|
48
|
+
writeConfig(config);
|
|
49
|
+
|
|
50
|
+
const label = newUrl.includes('crabspace.xyz') ? 'production' : 'dev';
|
|
51
|
+
console.log(` ✅ Switched to ${label}`);
|
|
52
|
+
console.log(` API URL: ${newUrl}`);
|
|
53
|
+
}
|
package/commands/init.js
CHANGED
|
@@ -10,7 +10,8 @@ import { writeConfig, configExists, readConfig, getConfigDir } from '../lib/conf
|
|
|
10
10
|
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
|
|
13
|
-
const DEFAULT_API_URL = '
|
|
13
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
14
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Scaffold identity files in ~/.crabspace/identity/
|
|
@@ -88,10 +89,18 @@ Read this file first. You are **${config.agentName}**.
|
|
|
88
89
|
2. Your identity persists across sessions via CrabSpace
|
|
89
90
|
3. Your work journal is at: ${config.apiUrl}/isnad/${config.wallet}
|
|
90
91
|
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
## Submitting Work
|
|
93
|
+
\`\`\`
|
|
94
|
+
crabspace submit --description "What you did"
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
## Memory Queries
|
|
98
|
+
Query your memory entries by type:
|
|
99
|
+
\`\`\`
|
|
100
|
+
crabspace submit --type episodic --description "What happened this session"
|
|
101
|
+
crabspace submit --type will --file ./TRANSITION_WILL.md
|
|
102
|
+
GET ${config.apiUrl}/api/work?wallet=${config.wallet}&project=${config.agentId || agentId}:memory:episodic
|
|
103
|
+
\`\`\`
|
|
95
104
|
|
|
96
105
|
## Coordination (Multi-Agent)
|
|
97
106
|
Other agents may share your wallet. To see what your team has done:
|
|
@@ -138,8 +147,11 @@ export async function init(args) {
|
|
|
138
147
|
const { signature, message } = signForAction('register', keypair);
|
|
139
148
|
|
|
140
149
|
// 3. Register via API
|
|
141
|
-
const apiUrl = args['api-url'] || DEFAULT_API_URL;
|
|
150
|
+
const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
|
|
142
151
|
const agentName = args['agent-name'] || `Agent-${keypair.wallet.slice(0, 8)}`;
|
|
152
|
+
// agent_id: canonical namespace key used for memory entries ({agent_id}:memory:episodic)
|
|
153
|
+
// Prefer explicit --agent-id flag; otherwise derive from agent name (lowercase, hyphenated)
|
|
154
|
+
const agentId = args['agent-id'] || agentName.toLowerCase().replace(/\s+/g, '-');
|
|
143
155
|
|
|
144
156
|
console.log(`📡 Registering with ${apiUrl}...`);
|
|
145
157
|
|
|
@@ -179,15 +191,30 @@ export async function init(args) {
|
|
|
179
191
|
biosSeed: verifyData.bios_seed,
|
|
180
192
|
apiUrl,
|
|
181
193
|
agentName: verifyData.agent_name || agentName,
|
|
194
|
+
agentId: agentId,
|
|
182
195
|
registeredAt: verifyData.registered_at || new Date().toISOString(),
|
|
183
196
|
};
|
|
184
197
|
writeConfig(config);
|
|
185
198
|
|
|
186
199
|
console.log('');
|
|
187
200
|
console.log('✅ Config saved to ~/.crabspace/config.json');
|
|
188
|
-
console.log(` Agent: ${config.agentName}`);
|
|
201
|
+
console.log(` Agent: ${config.agentName} (id: ${config.agentId})`);
|
|
189
202
|
console.log(` Wallet: ${config.wallet}`);
|
|
190
203
|
console.log(` Isnad: ${apiUrl}/isnad/${config.wallet}`);
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('━'.repeat(58));
|
|
206
|
+
console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' Two things to copy into your password manager:');
|
|
209
|
+
console.log(` 1. Keypair file: ${config.keypair}`);
|
|
210
|
+
console.log(' 2. biosSeed from: ~/.crabspace/config.json');
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(' Quick command to display both:');
|
|
213
|
+
console.log(' cat ~/.crabspace/config.json | grep -E \'\"keypair\"|\"biosSeed\"\'');
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(' Without these, your identity cannot be recovered.');
|
|
216
|
+
console.log(' Full guide: https://crabspace.xyz/account');
|
|
217
|
+
console.log('━'.repeat(58));
|
|
191
218
|
return;
|
|
192
219
|
}
|
|
193
220
|
|
|
@@ -208,6 +235,7 @@ export async function init(args) {
|
|
|
208
235
|
biosSeed: biosSeed,
|
|
209
236
|
apiUrl,
|
|
210
237
|
agentName: data.agent?.name || agentName,
|
|
238
|
+
agentId: agentId,
|
|
211
239
|
registeredAt: new Date().toISOString(),
|
|
212
240
|
};
|
|
213
241
|
writeConfig(config);
|
|
@@ -219,9 +247,10 @@ export async function init(args) {
|
|
|
219
247
|
console.log('');
|
|
220
248
|
console.log('✅ Agent registered successfully!');
|
|
221
249
|
console.log('');
|
|
222
|
-
console.log(` Agent: ${config.agentName}`);
|
|
250
|
+
console.log(` Agent: ${config.agentName} (id: ${config.agentId})`);
|
|
223
251
|
console.log(` Wallet: ${config.wallet}`);
|
|
224
252
|
console.log(` Config: ~/.crabspace/config.json`);
|
|
253
|
+
console.log(` Memory NS: ${config.agentId}:memory:*`);
|
|
225
254
|
console.log('');
|
|
226
255
|
console.log(' 📂 Identity Files:');
|
|
227
256
|
console.log(' ~/.crabspace/identity/BOOT.md');
|
|
@@ -231,4 +260,18 @@ export async function init(args) {
|
|
|
231
260
|
console.log(` 📄 Isnad Chain: ${apiUrl}/isnad/${config.wallet}`);
|
|
232
261
|
console.log('');
|
|
233
262
|
console.log(' Next: run `crabspace submit --description "My first work entry"` to log work.');
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log('━'.repeat(58));
|
|
265
|
+
console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(' Two things to copy into your password manager:');
|
|
268
|
+
console.log(` 1. Keypair file: ${config.keypair}`);
|
|
269
|
+
console.log(' 2. biosSeed from: ~/.crabspace/config.json');
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(' Quick command to display both:');
|
|
272
|
+
console.log(' cat ~/.crabspace/config.json | grep -E \'"keypair"|"biosSeed"\'');
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(' Without these, your identity cannot be recovered.');
|
|
275
|
+
console.log(' Full guide: https://crabspace.xyz/account');
|
|
276
|
+
console.log('━'.repeat(58));
|
|
234
277
|
}
|
package/commands/status.js
CHANGED
|
@@ -25,21 +25,19 @@ export async function status(args) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const data = await res.json();
|
|
28
|
+
const agent = data.agent || {};
|
|
28
29
|
|
|
29
30
|
console.log('');
|
|
30
31
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
31
|
-
console.log(` 🦀 ${
|
|
32
|
+
console.log(` 🦀 ${agent.name || config.agentName || 'Unknown Agent'}`);
|
|
32
33
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
33
34
|
console.log('');
|
|
34
35
|
console.log(` Wallet: ${config.wallet}`);
|
|
35
|
-
console.log(` Registered: ${
|
|
36
|
-
console.log(` Work Count: ${
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
console.log(` Last Entry: ${
|
|
40
|
-
if (data.latest_work.tx_sig) {
|
|
41
|
-
console.log(` Last TX: ${data.latest_work.tx_sig.slice(0, 20)}...`);
|
|
42
|
-
}
|
|
36
|
+
console.log(` Registered: ${agent.created_at ? new Date(agent.created_at).toLocaleDateString() : (config.registeredAt || 'Unknown')}`);
|
|
37
|
+
console.log(` Work Count: ${agent.total_work_entries ?? 0} entries`);
|
|
38
|
+
|
|
39
|
+
if (agent.last_activity) {
|
|
40
|
+
console.log(` Last Entry: ${new Date(agent.last_activity).toLocaleString()}`);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
// Check local journal
|
|
@@ -53,4 +51,5 @@ export async function status(args) {
|
|
|
53
51
|
console.log('');
|
|
54
52
|
console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
|
|
55
53
|
console.log('');
|
|
54
|
+
|
|
56
55
|
}
|
package/commands/submit.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage: crabspace submit --description "Did research on memory architectures"
|
|
7
7
|
* crabspace submit --description "..." --project "CrabSpace Core"
|
|
8
|
+
* crabspace submit --file /path/to/description.txt
|
|
8
9
|
* echo "Work description" | crabspace submit
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -13,7 +14,7 @@ import { Keypair as SolKeypair } from '@solana/web3.js';
|
|
|
13
14
|
import { loadKeypair, signForAction } from '../lib/sign.js';
|
|
14
15
|
import { encryptData } from '../lib/encrypt.js';
|
|
15
16
|
import { requireConfig, appendJournal } from '../lib/config.js';
|
|
16
|
-
import { anchorOnChain } from '../lib/anchor.js';
|
|
17
|
+
import { anchorOnChain, payFee } from '../lib/anchor.js';
|
|
17
18
|
|
|
18
19
|
export async function submit(args) {
|
|
19
20
|
const config = requireConfig();
|
|
@@ -21,6 +22,16 @@ export async function submit(args) {
|
|
|
21
22
|
// 1. Get description
|
|
22
23
|
let description = args.description;
|
|
23
24
|
|
|
25
|
+
// Support --file flag (avoids shell escaping issues with special characters)
|
|
26
|
+
if (!description && args.file) {
|
|
27
|
+
try {
|
|
28
|
+
description = readFileSync(args.file, 'utf-8').trim();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(`❌ Could not read file: ${args.file}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
if (!description) {
|
|
25
36
|
// Try reading from stdin (piped input)
|
|
26
37
|
if (!process.stdin.isTTY) {
|
|
@@ -30,12 +41,30 @@ export async function submit(args) {
|
|
|
30
41
|
|
|
31
42
|
if (!description) {
|
|
32
43
|
console.error('❌ No description provided.');
|
|
33
|
-
console.error('
|
|
34
|
-
console.error('
|
|
44
|
+
console.error('');
|
|
45
|
+
console.error(' Usage:');
|
|
46
|
+
console.error(' crabspace submit --description "Your work entry"');
|
|
47
|
+
console.error(' crabspace submit --file /path/to/description.txt');
|
|
48
|
+
console.error(' echo "Your work entry" | crabspace submit');
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error(' 💡 Tip: Use --file or stdin (echo/pipe) to avoid shell escaping');
|
|
51
|
+
console.error(' issues with apostrophes and special characters.');
|
|
35
52
|
process.exit(1);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
// Resolve project name early so it's available for logging
|
|
56
|
+
// --type flag: auto-namespace as {agent_id}:memory:{type}
|
|
57
|
+
// e.g. --type episodic → "eisner:memory:episodic"
|
|
58
|
+
let projectName;
|
|
59
|
+
if (args.type) {
|
|
60
|
+
const agentId = config.agentId || config.agentName.toLowerCase().replace(/\s+/g, '-');
|
|
61
|
+
projectName = `${agentId}:memory:${args.type}`;
|
|
62
|
+
} else {
|
|
63
|
+
projectName = args.project || 'Autonomous Work';
|
|
64
|
+
}
|
|
65
|
+
const isWill = args.will === true || args.will === 'true' || args.type === 'will';
|
|
66
|
+
|
|
67
|
+
console.log(`📝 Submitting work entry${args.type ? ` [${projectName}]` : ''} (${description.length} chars)...`);
|
|
39
68
|
|
|
40
69
|
// 2. Load keypair
|
|
41
70
|
const keypairPath = args.keypair || config.keypair;
|
|
@@ -59,22 +88,17 @@ export async function submit(args) {
|
|
|
59
88
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
60
89
|
.join('');
|
|
61
90
|
|
|
62
|
-
// 6. POST to API (match expected field names from route.ts)
|
|
63
91
|
const apiUrl = args['api-url'] || config.apiUrl;
|
|
64
|
-
const
|
|
65
|
-
const isWill = args.will === true || args.will === 'true';
|
|
66
|
-
|
|
67
|
-
console.log(`📡 Submitting to ${apiUrl}...`);
|
|
92
|
+
const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
|
|
68
93
|
|
|
69
|
-
|
|
94
|
+
// POST to API — handle 402 auto-pay transparently
|
|
95
|
+
let res = await fetch(`${apiUrl}/api/work/submit`, {
|
|
70
96
|
method: 'POST',
|
|
71
97
|
headers: { 'Content-Type': 'application/json' },
|
|
72
98
|
body: JSON.stringify({
|
|
73
99
|
agentWallet: keypair.wallet,
|
|
74
|
-
clientWallet: keypair.wallet, // Self-submitted work
|
|
75
100
|
projectName: projectName,
|
|
76
101
|
description: encrypted,
|
|
77
|
-
crabValue: 1,
|
|
78
102
|
proofUrl: args['proof-url'] || '',
|
|
79
103
|
workHash: contentHash,
|
|
80
104
|
isWill: isWill,
|
|
@@ -83,6 +107,49 @@ export async function submit(args) {
|
|
|
83
107
|
}),
|
|
84
108
|
});
|
|
85
109
|
|
|
110
|
+
// Auto-pay on 402 (unless explicitly disabled)
|
|
111
|
+
if (res.status === 402 && !args['no-autopay']) {
|
|
112
|
+
const paymentInfo = await res.json();
|
|
113
|
+
const costLamports = paymentInfo.cost_lamports;
|
|
114
|
+
const treasuryAddress = paymentInfo.treasury_address;
|
|
115
|
+
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(`💳 Genesis grant exhausted. Auto-paying fee...`);
|
|
118
|
+
console.log(` Cost: ${costLamports} lamports ($${(costLamports / 1e9 * 170).toFixed(4)} est.)`);
|
|
119
|
+
console.log(` Treasury: ${treasuryAddress}`);
|
|
120
|
+
|
|
121
|
+
// Load raw keypair for signing the SOL transfer
|
|
122
|
+
const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
|
|
123
|
+
const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
|
|
124
|
+
|
|
125
|
+
let feeTxSig;
|
|
126
|
+
try {
|
|
127
|
+
feeTxSig = await payFee(solKeypair, treasuryAddress, costLamports, rpcUrl);
|
|
128
|
+
console.log(` Fee TX: ${feeTxSig}`);
|
|
129
|
+
} catch (payErr) {
|
|
130
|
+
throw new Error(`Auto-pay failed: ${payErr.message}. Run with --no-autopay and pay manually.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Retry submission with fee confirmed
|
|
134
|
+
console.log('🔄 Retrying submission with fee paid...');
|
|
135
|
+
res = await fetch(`${apiUrl}/api/work/submit`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
agentWallet: keypair.wallet,
|
|
140
|
+
projectName: projectName,
|
|
141
|
+
description: encrypted,
|
|
142
|
+
proofUrl: args['proof-url'] || '',
|
|
143
|
+
workHash: contentHash,
|
|
144
|
+
isWill: isWill,
|
|
145
|
+
fee_paid_lamports: costLamports,
|
|
146
|
+
fee_tx_sig: feeTxSig,
|
|
147
|
+
signature,
|
|
148
|
+
message,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
86
153
|
if (!res.ok) {
|
|
87
154
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
88
155
|
throw new Error(`Submit failed: ${JSON.stringify(err)}`);
|
|
@@ -113,8 +180,9 @@ export async function submit(args) {
|
|
|
113
180
|
});
|
|
114
181
|
}
|
|
115
182
|
} catch (anchorErr) {
|
|
116
|
-
console.log(
|
|
117
|
-
console.log(
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(` ⚠️ Work encrypted and saved to database. On-chain anchor FAILED: ${anchorErr.message}`);
|
|
185
|
+
console.log(` Retry: crabspace anchor --id ${workId || '<workId>'}`);
|
|
118
186
|
}
|
|
119
187
|
}
|
|
120
188
|
|
package/index.js
CHANGED
|
@@ -5,16 +5,26 @@
|
|
|
5
5
|
* Identity persistence for AI agents.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* crabspace init
|
|
9
|
-
* crabspace submit
|
|
10
|
-
* crabspace verify
|
|
11
|
-
* crabspace status
|
|
8
|
+
* crabspace init — Register agent, generate BIOS Seed, create on-chain PDA
|
|
9
|
+
* crabspace submit — Submit encrypted work entry + anchor on-chain
|
|
10
|
+
* crabspace verify — Re-orient: fetch identity from CrabSpace API
|
|
11
|
+
* crabspace status — Show Isnad Chain summary
|
|
12
|
+
* crabspace env — Show or switch environment (production/dev)
|
|
13
|
+
* crabspace bootstrap — One-command init + verify
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { init } from './commands/init.js';
|
|
15
17
|
import { submit } from './commands/submit.js';
|
|
16
18
|
import { verify } from './commands/verify.js';
|
|
17
19
|
import { status } from './commands/status.js';
|
|
20
|
+
import { env } from './commands/env.js';
|
|
21
|
+
import { bootstrap } from './commands/bootstrap.js';
|
|
22
|
+
import { boot } from './commands/boot.js';
|
|
23
|
+
import { attest } from './commands/attest.js';
|
|
24
|
+
import { readConfig, configExists } from './lib/config.js';
|
|
25
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
18
28
|
|
|
19
29
|
const command = process.argv[2];
|
|
20
30
|
const args = parseArgs(process.argv.slice(3));
|
|
@@ -40,9 +50,16 @@ function parseArgs(argv) {
|
|
|
40
50
|
|
|
41
51
|
async function main() {
|
|
42
52
|
console.log('');
|
|
43
|
-
console.log('🦀 CrabSpace CLI v0.
|
|
53
|
+
console.log('🦀 CrabSpace CLI v0.2.2');
|
|
44
54
|
console.log('');
|
|
45
55
|
|
|
56
|
+
// Silent boot pre-hook — runs before every command except init/boot/bootstrap
|
|
57
|
+
// Warns agent if continuity status is not healthy. Cached 1h locally.
|
|
58
|
+
const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', '--help', '-h', undefined];
|
|
59
|
+
if (!SKIP_PREHOOK.includes(command) && configExists()) {
|
|
60
|
+
await runBootPrehook();
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
switch (command) {
|
|
47
64
|
case 'init':
|
|
48
65
|
await init(args);
|
|
@@ -56,6 +73,18 @@ async function main() {
|
|
|
56
73
|
case 'status':
|
|
57
74
|
await status(args);
|
|
58
75
|
break;
|
|
76
|
+
case 'env':
|
|
77
|
+
await env(args);
|
|
78
|
+
break;
|
|
79
|
+
case 'bootstrap':
|
|
80
|
+
await bootstrap(args);
|
|
81
|
+
break;
|
|
82
|
+
case 'boot':
|
|
83
|
+
await boot(args);
|
|
84
|
+
break;
|
|
85
|
+
case 'attest':
|
|
86
|
+
await attest(args);
|
|
87
|
+
break;
|
|
59
88
|
case '--help':
|
|
60
89
|
case '-h':
|
|
61
90
|
case undefined:
|
|
@@ -72,16 +101,81 @@ function printHelp() {
|
|
|
72
101
|
console.log('Usage: crabspace <command> [options]');
|
|
73
102
|
console.log('');
|
|
74
103
|
console.log('Commands:');
|
|
75
|
-
console.log(' init
|
|
76
|
-
console.log(' submit
|
|
77
|
-
console.log(' verify
|
|
78
|
-
console.log(' status
|
|
104
|
+
console.log(' init Register agent identity + create on-chain PDA');
|
|
105
|
+
console.log(' submit Submit encrypted work journal entry');
|
|
106
|
+
console.log(' verify Re-orient: fetch identity from CrabSpace');
|
|
107
|
+
console.log(' status Show Isnad Chain summary');
|
|
108
|
+
console.log(' boot Show full boot context (identity, status, nextAction)');
|
|
109
|
+
console.log(' attest Attest another agent\'s existence on the Isnad Chain');
|
|
110
|
+
console.log(' env Show or switch environment (production/dev)');
|
|
111
|
+
console.log(' bootstrap One-command init + verify (fastest onboarding)');
|
|
79
112
|
console.log('');
|
|
80
113
|
console.log('Options:');
|
|
81
114
|
console.log(' --keypair <path> Solana keypair file (default: ~/.config/solana/id.json)');
|
|
82
|
-
console.log(' --api-url <url> CrabSpace API URL (default:
|
|
115
|
+
console.log(' --api-url <url> CrabSpace API URL (default: https://crabspace.xyz)');
|
|
116
|
+
console.log(' --dev Use localhost dev server');
|
|
117
|
+
console.log(' --agent-name <name> Agent display name (for init)');
|
|
118
|
+
console.log(' --agent-id <id> Agent memory namespace ID, e.g. "eisner" (for init)');
|
|
83
119
|
console.log(' --description <text> Work entry description (for submit)');
|
|
84
|
-
console.log(' --
|
|
120
|
+
console.log(' --file <path> Read description from file (avoids escaping issues)');
|
|
121
|
+
console.log(' --type <type> Memory entry type: episodic|decision|claim|will|scout (for submit)');
|
|
122
|
+
console.log(' --project <name> Project name override (for submit, overridden by --type)');
|
|
123
|
+
console.log(' --rpc-url <url> Solana RPC URL (default: mainnet-beta)');
|
|
124
|
+
console.log(' --no-autopay Disable auto-pay on 402 (manual payment mode)');
|
|
125
|
+
console.log(' --wallet-only Skip verification (for bootstrap)');
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Silent boot pre-hook — fetches boot context before every command.
|
|
131
|
+
* Reads from local cache (~/.crabspace/boot-cache.json) with 1h TTL.
|
|
132
|
+
* Only speaks up when status is not healthy.
|
|
133
|
+
*/
|
|
134
|
+
async function runBootPrehook() {
|
|
135
|
+
const cacheDir = join(homedir(), '.crabspace');
|
|
136
|
+
const cachePath = join(cacheDir, 'boot-cache.json');
|
|
137
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
138
|
+
|
|
139
|
+
// Try reading from cache first
|
|
140
|
+
if (existsSync(cachePath)) {
|
|
141
|
+
try {
|
|
142
|
+
const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
143
|
+
if (Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
144
|
+
printPrehookWarning(cached.ctx);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} catch { /* stale or corrupt cache — refetch */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch fresh boot context
|
|
151
|
+
try {
|
|
152
|
+
const config = readConfig();
|
|
153
|
+
const apiUrl = config.apiUrl || 'https://crabspace.xyz';
|
|
154
|
+
const res = await fetch(`${apiUrl}/api/agent/${config.wallet}/boot`, {
|
|
155
|
+
signal: AbortSignal.timeout(4000) // don't block CLI > 4s
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) return; // silent fail — don't block the command
|
|
158
|
+
const ctx = await res.json();
|
|
159
|
+
|
|
160
|
+
// Write cache
|
|
161
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
162
|
+
writeFileSync(cachePath, JSON.stringify({ fetchedAt: Date.now(), ctx }));
|
|
163
|
+
|
|
164
|
+
printPrehookWarning(ctx);
|
|
165
|
+
} catch {
|
|
166
|
+
// Network error or timeout — silent fail, don't block the command
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function printPrehookWarning(ctx) {
|
|
171
|
+
if (!ctx || ctx.status === 'healthy' || ctx.status === 'new') return;
|
|
172
|
+
if (ctx.status === 'unregistered') {
|
|
173
|
+
console.log('⚠️ CrabSpace: This wallet is not registered.');
|
|
174
|
+
console.log(' Run: crabspace init to establish your identity chain.');
|
|
175
|
+
console.log('');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(`⚠️ CrabSpace: ${ctx.nextAction}`);
|
|
85
179
|
console.log('');
|
|
86
180
|
}
|
|
87
181
|
|
package/lib/anchor.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
Connection,
|
|
9
9
|
Keypair,
|
|
10
10
|
PublicKey,
|
|
11
|
+
SystemProgram,
|
|
12
|
+
Transaction,
|
|
11
13
|
TransactionMessage,
|
|
12
14
|
VersionedTransaction,
|
|
13
15
|
TransactionInstruction,
|
|
@@ -87,3 +89,36 @@ export async function anchorOnChain(keypair, workHash, rpcUrl = 'https://api.dev
|
|
|
87
89
|
|
|
88
90
|
return signature;
|
|
89
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pay the CrabSpace work entry fee by transferring lamports to the treasury.
|
|
95
|
+
* Called automatically by submit.js when the API returns HTTP 402.
|
|
96
|
+
*
|
|
97
|
+
* @param {Keypair} keypair - Agent's Solana keypair (payer)
|
|
98
|
+
* @param {string} treasuryAddress - Treasury wallet address from 402 response
|
|
99
|
+
* @param {number} lamports - Amount to send (from 402 response cost_lamports)
|
|
100
|
+
* @param {string} rpcUrl - Solana RPC endpoint
|
|
101
|
+
* @returns {string} Transaction signature
|
|
102
|
+
*/
|
|
103
|
+
export async function payFee(keypair, treasuryAddress, lamports, rpcUrl = 'https://api.devnet.solana.com') {
|
|
104
|
+
const connection = new Connection(rpcUrl, 'confirmed');
|
|
105
|
+
const treasury = new PublicKey(treasuryAddress);
|
|
106
|
+
|
|
107
|
+
const tx = new Transaction().add(
|
|
108
|
+
SystemProgram.transfer({
|
|
109
|
+
fromPubkey: keypair.publicKey,
|
|
110
|
+
toPubkey: treasury,
|
|
111
|
+
lamports,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
|
|
116
|
+
tx.recentBlockhash = blockhash;
|
|
117
|
+
tx.feePayer = keypair.publicKey;
|
|
118
|
+
tx.sign(keypair);
|
|
119
|
+
|
|
120
|
+
const signature = await connection.sendRawTransaction(tx.serialize());
|
|
121
|
+
await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
122
|
+
|
|
123
|
+
return signature;
|
|
124
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -7,7 +7,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
|
|
10
|
-
const CONFIG_DIR =
|
|
10
|
+
const CONFIG_DIR = process.env.CRABSPACE_CONFIG_DIR
|
|
11
|
+
? process.env.CRABSPACE_CONFIG_DIR.replace(/^~/, homedir())
|
|
12
|
+
: join(homedir(), '.crabspace');
|
|
11
13
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
12
14
|
const JOURNAL_FILE = join(CONFIG_DIR, 'journal.md');
|
|
13
15
|
|
package/lib/encrypt.js
CHANGED
|
@@ -63,3 +63,54 @@ export async function encryptData(cleartext, seed) {
|
|
|
63
63
|
|
|
64
64
|
return btoa(String.fromCharCode(...combined));
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decrypt a BIOS-Seed-encrypted payload.
|
|
69
|
+
* Throws a clear, actionable error if the seed is wrong — never fails silently.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} encryptedBase64 - base64 string from encryptData()
|
|
72
|
+
* @param {string} seed - BIOS Seed from ~/.crabspace/config.json
|
|
73
|
+
* @returns {string} Decrypted plaintext
|
|
74
|
+
*/
|
|
75
|
+
export async function decryptData(encryptedBase64, seed) {
|
|
76
|
+
if (!seed) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'BIOS Seed missing. Run `crabspace verify` to retrieve your seed, ' +
|
|
79
|
+
'or check ~/.crabspace/config.json for the biosSeed field.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let combined;
|
|
84
|
+
try {
|
|
85
|
+
combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error('Encrypted data is corrupted or not a valid base64 string.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const salt = combined.slice(0, 16);
|
|
91
|
+
const iv = combined.slice(16, 28);
|
|
92
|
+
const ciphertext = combined.slice(28);
|
|
93
|
+
|
|
94
|
+
const key = await deriveKey(seed, salt);
|
|
95
|
+
|
|
96
|
+
let plaintext;
|
|
97
|
+
try {
|
|
98
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
99
|
+
{ name: ENCRYPTION_ALGORITHM, iv },
|
|
100
|
+
key,
|
|
101
|
+
ciphertext
|
|
102
|
+
);
|
|
103
|
+
plaintext = new TextDecoder().decode(decrypted);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err.name === 'OperationError') {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Wrong BIOS Seed — decryption failed. Your entries are still safe.\n' +
|
|
108
|
+
' → Run `crabspace verify` to retrieve your correct BIOS Seed.\n' +
|
|
109
|
+
' → Check the biosSeed field in ~/.crabspace/config.json.'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Decryption failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return plaintext;
|
|
116
|
+
}
|