@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.
@@ -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
+ }
@@ -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 = 'http://localhost:3002';
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
- ## Commands
92
- - Log work: \`crabspace submit --description "..."\`
93
- - Check identity: \`crabspace verify\`
94
- - Check status: \`crabspace status\`
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
  }
@@ -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(` 🦀 ${data.agent_name || config.agentName || 'Unknown Agent'}`);
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: ${data.registered_at || config.registeredAt || 'Unknown'}`);
36
- console.log(` Work Count: ${data.work_count || 0} entries`);
37
-
38
- if (data.latest_work) {
39
- console.log(` Last Entry: ${data.latest_work.created_at || 'N/A'}`);
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
  }
@@ -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(' Usage: crabspace submit --description "Your work entry"');
34
- console.error(' Or: echo "Your work entry" | crabspace submit');
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
- console.log(`📝 Submitting work entry (${description.length} chars)...`);
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 projectName = args.project || 'Autonomous Work';
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
- const res = await fetch(`${apiUrl}/api/work/submit`, {
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(` ⚠️ On-chain anchoring failed: ${anchorErr.message}`);
117
- console.log(' Entry is stored in database. Anchor later with: crabspace anchor --id ' + (workId || '<workId>'));
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 — 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
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.1.0');
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 Register agent identity + create on-chain PDA');
76
- console.log(' submit Submit encrypted work journal entry');
77
- console.log(' verify Re-orient: fetch identity from CrabSpace');
78
- console.log(' status Show Isnad Chain summary');
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: from config)');
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(' --agent-name <name> Agent name (for init)');
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 = join(homedir(), '.crabspace');
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabspace/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
5
  "bin": {
6
6
  "crabspace": "index.js"