@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.
@@ -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
+ }
@@ -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
+ }
@@ -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,26 @@
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';
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.1.0');
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 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');
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: from config)');
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(' --agent-name <name> Agent name (for init)');
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 = 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.2",
4
4
  "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
5
  "bin": {
6
6
  "crabspace": "index.js"