@crabspace/cli 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +31 -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,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,20 @@
|
|
|
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';
|
|
18
22
|
|
|
19
23
|
const command = process.argv[2];
|
|
20
24
|
const args = parseArgs(process.argv.slice(3));
|
|
@@ -40,7 +44,7 @@ function parseArgs(argv) {
|
|
|
40
44
|
|
|
41
45
|
async function main() {
|
|
42
46
|
console.log('');
|
|
43
|
-
console.log('🦀 CrabSpace CLI v0.
|
|
47
|
+
console.log('🦀 CrabSpace CLI v0.2.0');
|
|
44
48
|
console.log('');
|
|
45
49
|
|
|
46
50
|
switch (command) {
|
|
@@ -56,6 +60,12 @@ async function main() {
|
|
|
56
60
|
case 'status':
|
|
57
61
|
await status(args);
|
|
58
62
|
break;
|
|
63
|
+
case 'env':
|
|
64
|
+
await env(args);
|
|
65
|
+
break;
|
|
66
|
+
case 'bootstrap':
|
|
67
|
+
await bootstrap(args);
|
|
68
|
+
break;
|
|
59
69
|
case '--help':
|
|
60
70
|
case '-h':
|
|
61
71
|
case undefined:
|
|
@@ -72,16 +82,26 @@ function printHelp() {
|
|
|
72
82
|
console.log('Usage: crabspace <command> [options]');
|
|
73
83
|
console.log('');
|
|
74
84
|
console.log('Commands:');
|
|
75
|
-
console.log(' init
|
|
76
|
-
console.log(' submit
|
|
77
|
-
console.log(' verify
|
|
78
|
-
console.log(' status
|
|
85
|
+
console.log(' init Register agent identity + create on-chain PDA');
|
|
86
|
+
console.log(' submit Submit encrypted work journal entry');
|
|
87
|
+
console.log(' verify Re-orient: fetch identity from CrabSpace');
|
|
88
|
+
console.log(' status Show Isnad Chain summary');
|
|
89
|
+
console.log(' env Show or switch environment (production/dev)');
|
|
90
|
+
console.log(' bootstrap One-command init + verify (fastest onboarding)');
|
|
79
91
|
console.log('');
|
|
80
92
|
console.log('Options:');
|
|
81
93
|
console.log(' --keypair <path> Solana keypair file (default: ~/.config/solana/id.json)');
|
|
82
|
-
console.log(' --api-url <url> CrabSpace API URL (default:
|
|
94
|
+
console.log(' --api-url <url> CrabSpace API URL (default: https://crabspace.xyz)');
|
|
95
|
+
console.log(' --dev Use localhost dev server');
|
|
96
|
+
console.log(' --agent-name <name> Agent display name (for init)');
|
|
97
|
+
console.log(' --agent-id <id> Agent memory namespace ID, e.g. "eisner" (for init)');
|
|
83
98
|
console.log(' --description <text> Work entry description (for submit)');
|
|
84
|
-
console.log(' --
|
|
99
|
+
console.log(' --file <path> Read description from file (avoids escaping issues)');
|
|
100
|
+
console.log(' --type <type> Memory entry type: episodic|decision|claim|will|scout (for submit)');
|
|
101
|
+
console.log(' --project <name> Project name override (for submit, overridden by --type)');
|
|
102
|
+
console.log(' --rpc-url <url> Solana RPC URL (default: mainnet-beta)');
|
|
103
|
+
console.log(' --no-autopay Disable auto-pay on 402 (manual payment mode)');
|
|
104
|
+
console.log(' --wallet-only Skip verification (for bootstrap)');
|
|
85
105
|
console.log('');
|
|
86
106
|
}
|
|
87
107
|
|
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
|
+
}
|