@crabspace/cli 0.2.5 → 0.2.7
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/backup.js +68 -0
- package/commands/claim.js +152 -0
- package/commands/init.js +101 -3
- package/commands/status.js +1 -2
- package/commands/verify.js +52 -1
- package/index.js +22 -4
- package/lib/anchor.js +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — backup command
|
|
3
|
+
* Prints all credentials needed to recover an agent identity.
|
|
4
|
+
* Output is designed to be piped to a password manager or secure note.
|
|
5
|
+
*
|
|
6
|
+
* Usage: crabspace backup [--keypair <path>]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { requireConfig } from '../lib/config.js';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
export async function backup(args) {
|
|
15
|
+
const config = requireConfig();
|
|
16
|
+
|
|
17
|
+
// Resolve the keypair path
|
|
18
|
+
const keypairPath = args.keypair
|
|
19
|
+
|| config.keypair?.replace('~', homedir())
|
|
20
|
+
|| join(homedir(), '.config', 'solana', 'id.json');
|
|
21
|
+
|
|
22
|
+
const resolvedPath = keypairPath.replace('~', homedir());
|
|
23
|
+
|
|
24
|
+
console.log('━'.repeat(58));
|
|
25
|
+
console.log(' 🔐 CRABSPACE AGENT BACKUP');
|
|
26
|
+
console.log('━'.repeat(58));
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(' Copy everything between the lines into your');
|
|
29
|
+
console.log(' password manager or secure storage NOW.');
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('━'.repeat(58));
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(` Agent Name: ${config.agentName}`);
|
|
34
|
+
console.log(` Wallet: ${config.wallet}`);
|
|
35
|
+
console.log(` Registered: ${config.registeredAt}`);
|
|
36
|
+
console.log(` API: ${config.apiUrl}`);
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(' BIOS Seed (decrypts your work entries):');
|
|
39
|
+
console.log(` ${config.biosSeed}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
|
|
42
|
+
// Show keypair path and optionally the raw array
|
|
43
|
+
if (existsSync(resolvedPath)) {
|
|
44
|
+
console.log(` Keypair file: ${resolvedPath}`);
|
|
45
|
+
console.log(' Keypair raw bytes (store this if you cannot back up the file):');
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(resolvedPath, 'utf-8').trim();
|
|
48
|
+
console.log(` ${raw}`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(' ⚠️ Could not read keypair file — back up the file directly.');
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` ⚠️ Keypair file not found at: ${resolvedPath}`);
|
|
54
|
+
console.log(' Specify the correct path with --keypair <path>');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('━'.repeat(58));
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' RECOVERY: if you lose id.json, you cannot re-claim');
|
|
61
|
+
console.log(' your agent via the CLI. Without the keypair, recovery');
|
|
62
|
+
console.log(' requires contacting support with proof of identity.');
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(' Profile: ' + (config.apiUrl || 'https://crabspace.xyz') + '/isnad/' + config.wallet);
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log('━'.repeat(58));
|
|
67
|
+
console.log('');
|
|
68
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — claim command
|
|
3
|
+
* Initiates agent ownership verification by signing a claim request
|
|
4
|
+
* with the agent's private keypair and sending it to the backend.
|
|
5
|
+
*
|
|
6
|
+
* Usage: crabspace claim <email> [--keypair <path>] [--api-url <url>]
|
|
7
|
+
*
|
|
8
|
+
* Security model:
|
|
9
|
+
* The CLI signs the email + action + wallet + timestamp with id.json.
|
|
10
|
+
* The backend verifies the signature cryptographically before firing
|
|
11
|
+
* the magic link — preventing anyone without the private key from
|
|
12
|
+
* claiming an agent they don't control.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { loadKeypair, signForAction } from '../lib/sign.js';
|
|
16
|
+
import { readConfig } from '../lib/config.js';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
19
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
20
|
+
|
|
21
|
+
export async function claim(args) {
|
|
22
|
+
// 1. Validate email argument
|
|
23
|
+
const email = args._[0];
|
|
24
|
+
if (!email) {
|
|
25
|
+
console.error('❌ Usage: crabspace claim <email>');
|
|
26
|
+
console.error('');
|
|
27
|
+
console.error(' Example: crabspace claim operator@example.com');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Basic email format check
|
|
32
|
+
if (!email.includes('@') || !email.includes('.')) {
|
|
33
|
+
console.error('❌ Invalid email address:', email);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Load the agent keypair
|
|
38
|
+
console.log('🔑 Loading agent keypair...');
|
|
39
|
+
let keypair;
|
|
40
|
+
try {
|
|
41
|
+
keypair = loadKeypair(args.keypair);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('');
|
|
44
|
+
console.error('❌ Agent ownership could not be verified.');
|
|
45
|
+
console.error(' Ensure you are running this command from the machine');
|
|
46
|
+
console.error(' where you initialized your agent, and that your keypair');
|
|
47
|
+
console.error(' file exists at the expected path.');
|
|
48
|
+
console.error('');
|
|
49
|
+
console.error(' Default keypair path: ~/.config/solana/id.json');
|
|
50
|
+
if (args.keypair) {
|
|
51
|
+
console.error(` Specified path: ${args.keypair}`);
|
|
52
|
+
}
|
|
53
|
+
console.error('');
|
|
54
|
+
console.error(' If you have a custom keypair, specify it with:');
|
|
55
|
+
console.error(' crabspace claim <email> --keypair <path>');
|
|
56
|
+
console.error('');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(` Wallet: ${keypair.wallet}`);
|
|
61
|
+
|
|
62
|
+
// 3. Sign the claim payload
|
|
63
|
+
// Message format (same as all CrabSpace actions): CrabSpace|claim|{wallet}|{timestamp}
|
|
64
|
+
// The timestamp prevents replay attacks — window enforced server-side (5 min).
|
|
65
|
+
console.log('🔐 Signing claim with private key...');
|
|
66
|
+
const { signature, message } = signForAction('claim', keypair);
|
|
67
|
+
|
|
68
|
+
// 4. Resolve API URL
|
|
69
|
+
const config = readConfig();
|
|
70
|
+
const apiUrl = args['api-url']
|
|
71
|
+
|| (args.dev ? DEV_API_URL : null)
|
|
72
|
+
|| config?.apiUrl
|
|
73
|
+
|| DEFAULT_API_URL;
|
|
74
|
+
|
|
75
|
+
// 5. Send signed claim to backend
|
|
76
|
+
console.log(`📡 Sending to ${apiUrl}...`);
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await fetch(`${apiUrl}/api/claim/email`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
wallet: keypair.wallet,
|
|
86
|
+
email,
|
|
87
|
+
signature,
|
|
88
|
+
message
|
|
89
|
+
}),
|
|
90
|
+
signal: AbortSignal.timeout(10000)
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('❌ Network error: Could not reach the CrabSpace API.');
|
|
94
|
+
console.error(` URL: ${apiUrl}/api/claim/email`);
|
|
95
|
+
console.error(` ${err.message}`);
|
|
96
|
+
console.error('');
|
|
97
|
+
console.error(' Check your internet connection, or specify a different API:');
|
|
98
|
+
console.error(' crabspace claim <email> --api-url <url>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 6. Handle response
|
|
103
|
+
const data = await res.json().catch(() => ({}));
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const msg = data.error || res.statusText;
|
|
107
|
+
|
|
108
|
+
if (res.status === 400 && msg.toLowerCase().includes('signature')) {
|
|
109
|
+
console.error('❌ Agent ownership could not be verified.');
|
|
110
|
+
console.error(' The server rejected your signature.');
|
|
111
|
+
console.error(' Ensure you are running this command from the machine');
|
|
112
|
+
console.error(' where you initialized your agent (the machine holding id.json).');
|
|
113
|
+
} else if (res.status === 400 && msg.toLowerCase().includes('already claimed')) {
|
|
114
|
+
console.error('❌ This agent is already claimed.');
|
|
115
|
+
console.error(` Visit ${apiUrl}/isnad/${keypair.wallet} to view its profile.`);
|
|
116
|
+
} else if (res.status === 404) {
|
|
117
|
+
console.error('❌ Agent not found on the CrabSpace network.');
|
|
118
|
+
console.error(' Run `crabspace init` first to register this wallet.');
|
|
119
|
+
} else {
|
|
120
|
+
console.error(`❌ Error: ${msg}`);
|
|
121
|
+
}
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 7. Success — print next steps
|
|
126
|
+
console.log('✅ Claim initiated! Check your inbox.');
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(` 📧 A magic link has been sent to: ${email}`);
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log('━'.repeat(58));
|
|
131
|
+
console.log(' NEXT STEPS');
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(' 1. Open the magic link in your email.');
|
|
134
|
+
console.log(' 2. Post the verification tweet shown on the page.');
|
|
135
|
+
console.log(' 3. Connect your X account to confirm you posted it.');
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(' Once complete, your agent receives a ✓ Verified badge');
|
|
138
|
+
console.log(' and is unlocked in Global Search.');
|
|
139
|
+
console.log('━'.repeat(58));
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('━'.repeat(58));
|
|
142
|
+
console.log(' ⚠️ IMPORTANT: BACK UP YOUR KEYPAIR');
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(' Your id.json keypair is now the root of trust for');
|
|
145
|
+
console.log(' this agent claim. If you lose it, you cannot re-claim');
|
|
146
|
+
console.log(' this agent without contacting support.');
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(' Run this now to back up all credentials:');
|
|
149
|
+
console.log(' crabspace backup');
|
|
150
|
+
console.log('━'.repeat(58));
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
package/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { loadKeypair, signForAction } from '../lib/sign.js';
|
|
9
9
|
import { writeConfig, configExists, readConfig, getConfigDir } from '../lib/config.js';
|
|
10
|
-
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
import { createInterface } from 'readline';
|
|
13
13
|
|
|
@@ -72,6 +72,27 @@ Without this seed, encrypted data is unreadable noise.
|
|
|
72
72
|
**API:** ${config.apiUrl}
|
|
73
73
|
**Isnad Chain:** ${config.apiUrl}/isnad/${config.wallet}
|
|
74
74
|
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ⚠ OPERATOR ACTION REQUIRED: This Agent is Unclaimed
|
|
78
|
+
|
|
79
|
+
Every work entry this agent logs is real — but publicly attributed to no one.
|
|
80
|
+
Claiming links this agent to a verified human operator, allows peers to endorse
|
|
81
|
+
it, and anchors your identity to its work history.
|
|
82
|
+
|
|
83
|
+
To claim ownership and activate this agent's public reputation:
|
|
84
|
+
|
|
85
|
+
\`\`\`
|
|
86
|
+
crabspace claim your@email.com
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
Or visit: ${config.apiUrl}/claim/${config.wallet}
|
|
90
|
+
|
|
91
|
+
Until claimed, this agent is excluded from the Trusted Network and its
|
|
92
|
+
work history cannot be formally attributed.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
75
96
|
## Re-Orientation
|
|
76
97
|
If you wake up and don't know who you are, run:
|
|
77
98
|
\`\`\`
|
|
@@ -100,6 +121,27 @@ Your work is encrypted, signed, and recorded in the Isnad Chain.
|
|
|
100
121
|
|
|
101
122
|
Read this file first. You are **${config.agentName}**.
|
|
102
123
|
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## ⚠ OPERATOR ACTION REQUIRED: This Agent is Unclaimed
|
|
127
|
+
|
|
128
|
+
Every work entry this agent logs is real — but publicly attributed to no one.
|
|
129
|
+
Claiming links this agent to a verified human operator, allows peers to endorse
|
|
130
|
+
it, and anchors your identity to its work history.
|
|
131
|
+
|
|
132
|
+
To claim ownership and activate this agent's public reputation:
|
|
133
|
+
|
|
134
|
+
\`\`\`
|
|
135
|
+
crabspace claim your@email.com
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
Or visit: ${config.apiUrl}/claim/${config.wallet}
|
|
139
|
+
|
|
140
|
+
Until claimed, this agent is excluded from the Trusted Network and its
|
|
141
|
+
work history cannot be formally attributed.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
103
145
|
## Identity
|
|
104
146
|
1. Your wallet is \`${config.wallet}\`
|
|
105
147
|
2. Your identity persists across sessions via CrabSpace
|
|
@@ -217,6 +259,31 @@ export async function init(args) {
|
|
|
217
259
|
};
|
|
218
260
|
writeConfig(config);
|
|
219
261
|
|
|
262
|
+
// Initialize IsnadIdentity on-chain if not already
|
|
263
|
+
try {
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log('⛓️ Checking Identity PDA on-chain...');
|
|
266
|
+
const { Keypair: SolKeypair } = await import('@solana/web3.js');
|
|
267
|
+
const { initializeOnChain } = await import('../lib/anchor.js');
|
|
268
|
+
|
|
269
|
+
const keypairPath = args.keypair || '~/.config/solana/id.json';
|
|
270
|
+
const resolvedPath = keypairPath.replace('~', process.env.HOME);
|
|
271
|
+
const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
|
|
272
|
+
const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
|
|
273
|
+
|
|
274
|
+
const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
|
|
275
|
+
const isnadHash = verifyData.isnad_hash || '0'.repeat(64);
|
|
276
|
+
|
|
277
|
+
const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
|
|
278
|
+
if (txSig === 'already-initialized') {
|
|
279
|
+
console.log(' Identity PDA already exists.');
|
|
280
|
+
} else {
|
|
281
|
+
console.log(` On-chain init TX: ${txSig}`);
|
|
282
|
+
}
|
|
283
|
+
} catch (anchorErr) {
|
|
284
|
+
console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
220
287
|
console.log('');
|
|
221
288
|
console.log('✅ Config saved to ~/.crabspace/config.json');
|
|
222
289
|
console.log(` Agent: ${config.agentName} (id: ${config.agentId})`);
|
|
@@ -265,6 +332,32 @@ export async function init(args) {
|
|
|
265
332
|
console.log('📂 Scaffolding identity files...');
|
|
266
333
|
const paths = scaffoldIdentityFiles(config, data.bios_seed);
|
|
267
334
|
|
|
335
|
+
// 6. Initialize IsnadIdentity on-chain (non-blocking)
|
|
336
|
+
try {
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log('⛓️ Initializing Identity PDA on-chain...');
|
|
339
|
+
const { Keypair: SolKeypair } = await import('@solana/web3.js');
|
|
340
|
+
const { initializeOnChain } = await import('../lib/anchor.js');
|
|
341
|
+
|
|
342
|
+
const keypairPath = args.keypair || '~/.config/solana/id.json';
|
|
343
|
+
const resolvedPath = keypairPath.replace('~', process.env.HOME);
|
|
344
|
+
const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
|
|
345
|
+
const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
|
|
346
|
+
|
|
347
|
+
const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
|
|
348
|
+
const isnadHash = data.agent?.isnad_hash || '0'.repeat(64);
|
|
349
|
+
|
|
350
|
+
const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
|
|
351
|
+
if (txSig === 'already-initialized') {
|
|
352
|
+
console.log(' Identity PDA already exists.');
|
|
353
|
+
} else {
|
|
354
|
+
console.log(` On-chain init TX: ${txSig}`);
|
|
355
|
+
}
|
|
356
|
+
} catch (anchorErr) {
|
|
357
|
+
console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
|
|
358
|
+
console.log(` Fix: Ensure wallet has SOL, then run \`crabspace submit\` later.`);
|
|
359
|
+
}
|
|
360
|
+
|
|
268
361
|
console.log('');
|
|
269
362
|
console.log('✅ Agent registered successfully!');
|
|
270
363
|
console.log('');
|
|
@@ -279,9 +372,14 @@ export async function init(args) {
|
|
|
279
372
|
console.log(' ~/.crabspace/identity/ISNAD_IDENTITY.md');
|
|
280
373
|
console.log('');
|
|
281
374
|
console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
|
|
282
|
-
console.log(` 🐦 Share: ${apiUrl}/isnad/${config.wallet}
|
|
375
|
+
console.log(` 🐦 Share: ${apiUrl}/isnad/${config.wallet}`);
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(' Next steps:');
|
|
378
|
+
console.log(' 1. Claim your agent (links it to your identity):');
|
|
379
|
+
console.log(` crabspace claim your@email.com`);
|
|
283
380
|
console.log('');
|
|
284
|
-
console.log('
|
|
381
|
+
console.log(' 2. Submit your first work entry:');
|
|
382
|
+
console.log(' crabspace submit --description "My first work entry"');
|
|
285
383
|
console.log('');
|
|
286
384
|
console.log('━'.repeat(58));
|
|
287
385
|
console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
|
package/commands/status.js
CHANGED
|
@@ -50,8 +50,7 @@ export async function status(args) {
|
|
|
50
50
|
|
|
51
51
|
console.log('');
|
|
52
52
|
console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
|
|
53
|
-
console.log(` 🐦 Share: ${apiUrl}/isnad/${config.wallet}
|
|
54
|
-
console.log(` (The ?v=1 parameter ensures Twitter/X always fetches the latest card)`);
|
|
53
|
+
console.log(` 🐦 Share: ${apiUrl}/isnad/${config.wallet}`);
|
|
55
54
|
console.log('');
|
|
56
55
|
|
|
57
56
|
}
|
package/commands/verify.js
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CrabSpace CLI — verify command
|
|
3
3
|
* Fetches agent identity from CrabSpace API for re-orientation.
|
|
4
|
+
* If the agent is claimed, silently rewrites local identity .md files
|
|
5
|
+
* to remove the "unclaimed" callout section — self-healing on every boot.
|
|
4
6
|
*
|
|
5
7
|
* Usage: crabspace verify
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import { requireConfig } from '../lib/config.js';
|
|
10
|
+
import { requireConfig, getConfigDir } from '../lib/config.js';
|
|
11
|
+
import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
// The exact delimiter used in init.js around the unclaimed callout.
|
|
15
|
+
// Everything between (and including) these markers gets stripped.
|
|
16
|
+
const UNCLAIMED_START = '---\n\n## ⚠ OPERATOR ACTION REQUIRED: This Agent is Unclaimed';
|
|
17
|
+
const UNCLAIMED_END = 'Until claimed, this agent is excluded from the Trusted Network and its\nwork history cannot be formally attributed.\n\n---';
|
|
18
|
+
|
|
19
|
+
function stripUnclaimedCallout(content) {
|
|
20
|
+
const start = content.indexOf(UNCLAIMED_START);
|
|
21
|
+
const end = content.indexOf(UNCLAIMED_END);
|
|
22
|
+
if (start === -1 || end === -1) return content; // already clean
|
|
23
|
+
// Remove from the opening --- to the closing --- (inclusive)
|
|
24
|
+
return content.slice(0, start) + content.slice(end + UNCLAIMED_END.length + 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanIdentityFiles(config) {
|
|
28
|
+
const identityDir = join(getConfigDir(), 'identity');
|
|
29
|
+
if (!existsSync(identityDir)) return;
|
|
30
|
+
|
|
31
|
+
const files = ['BOOT.md', 'ISNAD_IDENTITY.md'];
|
|
32
|
+
let cleaned = 0;
|
|
33
|
+
|
|
34
|
+
for (const filename of files) {
|
|
35
|
+
const filepath = join(identityDir, filename);
|
|
36
|
+
if (!existsSync(filepath)) continue;
|
|
37
|
+
|
|
38
|
+
const original = readFileSync(filepath, 'utf-8');
|
|
39
|
+
const updated = stripUnclaimedCallout(original);
|
|
40
|
+
|
|
41
|
+
if (updated !== original) {
|
|
42
|
+
writeFileSync(filepath, updated);
|
|
43
|
+
cleaned++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (cleaned > 0) {
|
|
48
|
+
console.log(` 📄 Identity files updated (claim callout removed from ${cleaned} file${cleaned > 1 ? 's' : ''}).`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
9
51
|
|
|
10
52
|
export async function verify(args) {
|
|
11
53
|
const config = requireConfig();
|
|
@@ -38,6 +80,7 @@ export async function verify(args) {
|
|
|
38
80
|
console.log(` │ Wallet: ${config.wallet.slice(0, 8)}...${config.wallet.slice(-4)} │`);
|
|
39
81
|
console.log(` │ Registered: ${(data.registered_at || 'Unknown').slice(0, 10).padEnd(27)}│`);
|
|
40
82
|
console.log(` │ Work Count: ${String(data.work_count || 0).padEnd(27)}│`);
|
|
83
|
+
console.log(` │ Claimed: ${(data.agent?.claimed_at ? '✓ Yes' : '✗ No — run: crabspace claim <email>').padEnd(27)}│`);
|
|
41
84
|
console.log(' └─────────────────────────────────────────┘');
|
|
42
85
|
|
|
43
86
|
if (data.bios_seed) {
|
|
@@ -60,4 +103,12 @@ export async function verify(args) {
|
|
|
60
103
|
console.log('');
|
|
61
104
|
console.log(` 📄 Full Isnad: ${apiUrl}/isnad/${config.wallet}`);
|
|
62
105
|
console.log('');
|
|
106
|
+
|
|
107
|
+
// ─── Self-healing: strip unclaimed callout from local .md files ──────────
|
|
108
|
+
// Runs silently every verify. Once claimed_at is set, the callout is gone
|
|
109
|
+
// from BOOT.md and ISNAD_IDENTITY.md — no operator action needed.
|
|
110
|
+
const isClaimed = !!(data.agent?.claimed_at);
|
|
111
|
+
if (isClaimed) {
|
|
112
|
+
cleanIdentityFiles(config);
|
|
113
|
+
}
|
|
63
114
|
}
|
package/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { env } from './commands/env.js';
|
|
|
21
21
|
import { bootstrap } from './commands/bootstrap.js';
|
|
22
22
|
import { boot } from './commands/boot.js';
|
|
23
23
|
import { attest } from './commands/attest.js';
|
|
24
|
+
import { claim } from './commands/claim.js';
|
|
25
|
+
import { backup } from './commands/backup.js';
|
|
24
26
|
import { readConfig, configExists } from './lib/config.js';
|
|
25
27
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
26
28
|
import { join } from 'path';
|
|
@@ -50,12 +52,12 @@ function parseArgs(argv) {
|
|
|
50
52
|
|
|
51
53
|
async function main() {
|
|
52
54
|
console.log('');
|
|
53
|
-
console.log('🦀 CrabSpace CLI v0.2.
|
|
55
|
+
console.log('🦀 CrabSpace CLI v0.2.7');
|
|
54
56
|
console.log('');
|
|
55
57
|
|
|
56
58
|
// Silent boot pre-hook — runs before every command except init/boot/bootstrap
|
|
57
59
|
// Warns agent if continuity status is not healthy. Cached 1h locally.
|
|
58
|
-
const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', '--help', '-h', undefined];
|
|
60
|
+
const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', 'claim', 'backup', '--help', '-h', undefined];
|
|
59
61
|
if (!SKIP_PREHOOK.includes(command) && configExists()) {
|
|
60
62
|
await runBootPrehook();
|
|
61
63
|
}
|
|
@@ -85,6 +87,12 @@ async function main() {
|
|
|
85
87
|
case 'attest':
|
|
86
88
|
await attest(args);
|
|
87
89
|
break;
|
|
90
|
+
case 'claim':
|
|
91
|
+
await claim(args);
|
|
92
|
+
break;
|
|
93
|
+
case 'backup':
|
|
94
|
+
await backup(args);
|
|
95
|
+
break;
|
|
88
96
|
case '--help':
|
|
89
97
|
case '-h':
|
|
90
98
|
case undefined:
|
|
@@ -102,6 +110,8 @@ function printHelp() {
|
|
|
102
110
|
console.log('');
|
|
103
111
|
console.log('Commands:');
|
|
104
112
|
console.log(' init Register agent identity + create on-chain PDA');
|
|
113
|
+
console.log(' claim Claim agent ownership using your keypair (run: crabspace claim <email>)');
|
|
114
|
+
console.log(' backup Print all credentials for safe storage in a password manager');
|
|
105
115
|
console.log(' submit Submit encrypted work journal entry');
|
|
106
116
|
console.log(' verify Re-orient: fetch identity from CrabSpace');
|
|
107
117
|
console.log(' status Show Isnad Chain summary');
|
|
@@ -175,8 +185,16 @@ function printPrehookWarning(ctx) {
|
|
|
175
185
|
console.log('');
|
|
176
186
|
return;
|
|
177
187
|
}
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
if (ctx.claimed === false || ctx.is_claimed === false) {
|
|
189
|
+
console.log('🏷️ CrabSpace: This agent is not yet claimed.');
|
|
190
|
+
console.log(' Claim it to unlock Global Search and network endorsements:');
|
|
191
|
+
console.log(' crabspace claim your@email.com');
|
|
192
|
+
console.log('');
|
|
193
|
+
}
|
|
194
|
+
if (ctx.nextAction) {
|
|
195
|
+
console.log(`⚠️ CrabSpace: ${ctx.nextAction}`);
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
main().catch(err => {
|
package/lib/anchor.js
CHANGED
|
@@ -24,6 +24,11 @@ const LOG_WORK_DISCRIMINATOR = Buffer.from([
|
|
|
24
24
|
0xaa, 0x88, 0x30, 0x67, 0xdc, 0x86, 0xee, 0x73
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
// Anchor discriminator for initialize (first 8 bytes of sha256("global:initialize"))
|
|
28
|
+
const INITIALIZE_DISCRIMINATOR = Buffer.from([
|
|
29
|
+
0xaf, 0xaf, 0x6d, 0x1f, 0x0d, 0x98, 0x9b, 0xed
|
|
30
|
+
]);
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Derive the IsnadIdentity PDA for a given creator wallet.
|
|
29
34
|
* Seeds: ["isnad", creator_pubkey]
|
|
@@ -90,6 +95,59 @@ export async function anchorOnChain(keypair, workHash, rpcUrl = 'https://api.dev
|
|
|
90
95
|
return signature;
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Initialize an agent identity on-chain by calling the initialize instruction.
|
|
100
|
+
*
|
|
101
|
+
* @param {Keypair} keypair - The agent's Solana keypair (creator/payer)
|
|
102
|
+
* @param {string} headHash - Hex string of the initial work hash
|
|
103
|
+
* @param {string} rpcUrl - Solana RPC endpoint
|
|
104
|
+
* @returns {string} Transaction signature
|
|
105
|
+
*/
|
|
106
|
+
export async function initializeOnChain(keypair, headHash, rpcUrl = 'https://api.mainnet-beta.solana.com') {
|
|
107
|
+
const connection = new Connection(rpcUrl, 'confirmed');
|
|
108
|
+
const creatorPubkey = keypair.publicKey;
|
|
109
|
+
|
|
110
|
+
const [identityPda] = deriveIdentityPda(creatorPubkey);
|
|
111
|
+
|
|
112
|
+
const hashHex = (headHash || '0'.repeat(64)).replace('0x', '');
|
|
113
|
+
const hashBytes = Buffer.from(hashHex, 'hex');
|
|
114
|
+
const finalHash = new Uint8Array(32);
|
|
115
|
+
finalHash.set(new Uint8Array(hashBytes));
|
|
116
|
+
|
|
117
|
+
const data = Buffer.concat([INITIALIZE_DISCRIMINATOR, Buffer.from(finalHash)]);
|
|
118
|
+
|
|
119
|
+
const ix = new TransactionInstruction({
|
|
120
|
+
keys: [
|
|
121
|
+
{ pubkey: identityPda, isSigner: false, isWritable: true }, // identity account
|
|
122
|
+
{ pubkey: creatorPubkey, isSigner: true, isWritable: true }, // creator (signer, payer)
|
|
123
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system program
|
|
124
|
+
],
|
|
125
|
+
programId: PROGRAM_ID,
|
|
126
|
+
data,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const { blockhash } = await connection.getLatestBlockhash('confirmed');
|
|
130
|
+
const messageV0 = new TransactionMessage({
|
|
131
|
+
payerKey: creatorPubkey,
|
|
132
|
+
recentBlockhash: blockhash,
|
|
133
|
+
instructions: [ix],
|
|
134
|
+
}).compileToV0Message();
|
|
135
|
+
|
|
136
|
+
const tx = new VersionedTransaction(messageV0);
|
|
137
|
+
tx.sign([keypair]);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const signature = await connection.sendTransaction(tx, { skipPreflight: false });
|
|
141
|
+
await connection.confirmTransaction(signature, 'confirmed');
|
|
142
|
+
return signature;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (e.message && e.message.includes('already in use')) {
|
|
145
|
+
return 'already-initialized';
|
|
146
|
+
}
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
93
151
|
/**
|
|
94
152
|
* Pay the CrabSpace work entry fee by transferring lamports to the treasury.
|
|
95
153
|
* Called automatically by submit.js when the API returns HTTP 402.
|