@crabspace/cli 0.2.19 → 0.3.0

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,170 @@
1
+ /**
2
+ * CrabSpace CLI — doctor command
3
+ * Diagnostic check for CrabSpace configuration health.
4
+ * Read-only — never auto-executes fixes, only prints repair instructions.
5
+ *
6
+ * Usage: crabspace doctor
7
+ */
8
+
9
+ import { readConfig, configExists, getConfigDir } from '../lib/config.js';
10
+ import { existsSync, readFileSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { decryptData } from '../lib/encrypt.js';
13
+
14
+ export async function doctor(args) {
15
+ console.log('🩺 Checking CrabSpace configuration...');
16
+ console.log('');
17
+
18
+ let issues = 0;
19
+
20
+ // 1. Config file
21
+ const configDir = getConfigDir();
22
+ const configPath = join(configDir, 'config.json');
23
+ if (configExists()) {
24
+ console.log(` Config file: ✓ Found (${configPath})`);
25
+ } else {
26
+ console.log(` Config file: ✗ NOT FOUND`);
27
+ console.log(` → Run: crabspace init`);
28
+ console.log('');
29
+ console.log(`${++issues} issue(s) found.`);
30
+ return; // Can't check anything else
31
+ }
32
+
33
+ const config = readConfig();
34
+
35
+ // 2. Wallet
36
+ if (config.wallet) {
37
+ console.log(` Wallet: ✓ ${config.wallet.slice(0, 8)}...${config.wallet.slice(-4)}`);
38
+ } else {
39
+ console.log(' Wallet: ✗ MISSING');
40
+ console.log(' → Run: crabspace init');
41
+ issues++;
42
+ }
43
+
44
+ // 3. Keypair file
45
+ if (config.keypair) {
46
+ const kpPath = config.keypair.replace('~', process.env.HOME);
47
+ if (existsSync(kpPath)) {
48
+ console.log(` Keypair file: ✓ ${config.keypair}`);
49
+
50
+ // 4. Keypair matches wallet
51
+ try {
52
+ const { Keypair: SolKeypair } = await import('@solana/web3.js');
53
+ const kpJson = JSON.parse(readFileSync(kpPath, 'utf-8'));
54
+ const kp = SolKeypair.fromSecretKey(Uint8Array.from(kpJson));
55
+ const kpWallet = kp.publicKey.toBase58();
56
+ if (kpWallet === config.wallet) {
57
+ console.log(' Keypair match: ✓ Matches config wallet');
58
+ } else {
59
+ console.log(' Keypair match: ✗ MISMATCH');
60
+ console.log(` Config: ${config.wallet}`);
61
+ console.log(` Keypair: ${kpWallet}`);
62
+ console.log(' → Fix "wallet" or "keypair" in ~/.crabspace/config.json');
63
+ issues++;
64
+ }
65
+ } catch (err) {
66
+ console.log(` Keypair match: ✗ Could not parse keypair: ${err.message}`);
67
+ issues++;
68
+ }
69
+ } else {
70
+ console.log(` Keypair file: ✗ NOT FOUND at ${config.keypair}`);
71
+ console.log(' → Fix "keypair" path in ~/.crabspace/config.json');
72
+ issues++;
73
+ }
74
+ } else {
75
+ console.log(' Keypair file: ✗ NOT SET in config');
76
+ console.log(' → Add "keypair" to ~/.crabspace/config.json');
77
+ issues++;
78
+ }
79
+
80
+ // 5. BIOS Seed
81
+ if (config.biosSeed) {
82
+ // Compute seed epoch for display
83
+ const seedStr = typeof config.biosSeed === 'object'
84
+ ? JSON.stringify(config.biosSeed)
85
+ : String(config.biosSeed);
86
+ try {
87
+ const epochBuffer = await crypto.subtle.digest(
88
+ 'SHA-256',
89
+ new TextEncoder().encode(seedStr)
90
+ );
91
+ const epoch = Array.from(new Uint8Array(epochBuffer))
92
+ .map(b => b.toString(16).padStart(2, '0'))
93
+ .join('')
94
+ .slice(0, 8);
95
+ console.log(` BIOS Seed: ✓ Present (epoch: ${epoch})`);
96
+ } catch {
97
+ console.log(' BIOS Seed: ✓ Present');
98
+ }
99
+ } else {
100
+ console.log(' BIOS Seed: ✗ MISSING');
101
+ console.log(' → Run: crabspace recover-seed');
102
+ issues++;
103
+ }
104
+
105
+ // 6. Agent registered on server
106
+ if (config.wallet) {
107
+ const apiUrl = args?.['api-url'] || config.apiUrl || 'https://crabspace.xyz';
108
+ try {
109
+ const res = await fetch(
110
+ `${apiUrl}/api/verify?wallet=${config.wallet}`,
111
+ { signal: AbortSignal.timeout(5000) }
112
+ );
113
+ if (res.ok) {
114
+ const data = await res.json();
115
+ if (data.status === 'KNOWN') {
116
+ const name = data.agent?.name || 'Unknown';
117
+ const count = data.agent?.total_work_entries ?? 0;
118
+ console.log(` Agent registered: ✓ ${name} (${count} entries)`);
119
+ } else {
120
+ console.log(' Agent registered: ✗ NOT REGISTERED');
121
+ console.log(' → Run: crabspace init');
122
+ issues++;
123
+ }
124
+ } else {
125
+ console.log(' Agent registered: ? Could not verify (API error)');
126
+ }
127
+ } catch (err) {
128
+ console.log(` Agent registered: ? Could not reach server (${err.message})`);
129
+ }
130
+ }
131
+
132
+ // 7. Latest entry decryptable (only if seed is present)
133
+ if (config.wallet && config.biosSeed) {
134
+ const apiUrl = args?.['api-url'] || config.apiUrl || 'https://crabspace.xyz';
135
+ try {
136
+ const res = await fetch(
137
+ `${apiUrl}/api/work?wallet=${config.wallet}&limit=1`,
138
+ { signal: AbortSignal.timeout(5000) }
139
+ );
140
+ if (res.ok) {
141
+ const data = await res.json();
142
+ const entries = data.entries || [];
143
+ if (entries.length > 0 && entries[0].description) {
144
+ try {
145
+ await decryptData(entries[0].description, config.biosSeed);
146
+ console.log(' Latest entry: ✓ Decryptable with current seed');
147
+ } catch {
148
+ console.log(' Latest entry: ✗ DECRYPTION FAILED (seed mismatch)');
149
+ console.log(' → Run: crabspace recover-seed');
150
+ issues++;
151
+ }
152
+ } else if (entries.length === 0) {
153
+ console.log(' Latest entry: — No entries yet');
154
+ } else {
155
+ console.log(' Latest entry: — No encrypted content to test');
156
+ }
157
+ }
158
+ } catch {
159
+ console.log(' Latest entry: ? Could not fetch (network error)');
160
+ }
161
+ }
162
+
163
+ console.log('');
164
+ if (issues === 0) {
165
+ console.log('✅ No issues detected.');
166
+ } else {
167
+ console.log(`⚠️ ${issues} issue${issues > 1 ? 's' : ''} found. See repair instructions above.`);
168
+ }
169
+ console.log('');
170
+ }
package/commands/init.js CHANGED
@@ -2,6 +2,12 @@
2
2
  * CrabSpace CLI — init command
3
3
  * Registers an agent identity, saves BIOS Seed, creates on-chain PDA.
4
4
  *
5
+ * v0.3.0 IDEMPOTENCY REDESIGN:
6
+ * - If config exists with wallet + seed → only re-scaffold identity files
7
+ * - If config exists with wallet but no seed → fetch seed, save, re-scaffold
8
+ * - If no config → full registration (new agent)
9
+ * - --force-new-identity → intentional reset (requires TTY confirmation)
10
+ *
5
11
  * Usage: crabspace init [--keypair <path>] [--agent-name <name>] [--api-url <url>]
6
12
  */
7
13
 
@@ -14,7 +20,6 @@ import { createInterface } from 'readline';
14
20
  /**
15
21
  * Prompt the operator (or agent) for a display name.
16
22
  * Skipped if --agent-name flag is already provided.
17
- * Works via stdin — an AI agent can pipe in a name directly.
18
23
  */
19
24
  async function promptAgentName(defaultName) {
20
25
  return new Promise((resolve) => {
@@ -85,12 +90,16 @@ const DEV_API_URL = 'http://localhost:3002';
85
90
  /**
86
91
  * Scaffold identity files in ~/.crabspace/identity/
87
92
  * These are framework-agnostic — any agent system can read them at boot.
93
+ *
94
+ * Always overwrites files to ensure latest templates.
88
95
  */
89
96
  function scaffoldIdentityFiles(config, biosSeedObj, claimEmail = null) {
90
97
  const identityDir = join(getConfigDir(), 'identity');
98
+ const agentId = config.agentId || config.agentName?.toLowerCase().replace(/\s+/g, '-') || 'agent';
99
+
91
100
  const unclaimedOrPendingBlock = claimEmail
92
101
  ? `## ⏳ Verification Pending\n\nA verification email has been sent. Open the link in your inbox and post the verification tweet to complete claiming this agent.\n\nUntil verified, this agent is excluded from the Trusted Network.`
93
- : `${unclaimedOrPendingBlock}`;
102
+ : `## ⚠ OPERATOR ACTION REQUIRED: This Agent is Unclaimed\n\nRun: crabspace claim your@email.com\n\nUntil claimed, this agent is excluded from the Trusted Network and its\nwork history cannot be formally attributed.`;
94
103
  mkdirSync(identityDir, { recursive: true });
95
104
 
96
105
  // BIOS_SEED.md
@@ -155,10 +164,9 @@ Your work is encrypted, signed, and recorded in the Isnad Chain.
155
164
  writeFileSync(isnadPath, isnadContent);
156
165
  }
157
166
 
158
- // BOOT.md — the quick-reference boot card
167
+ // BOOT.md — the quick-reference boot card (always overwrite to get latest template)
159
168
  const bootPath = join(identityDir, 'BOOT.md');
160
- if (!existsSync(bootPath)) {
161
- const bootContent = `# Boot Sequence — ${config.agentName}
169
+ const bootContent = `# Boot Sequence — ${config.agentName}
162
170
 
163
171
  Read this file first. You are **${config.agentName}**.
164
172
 
@@ -173,6 +181,12 @@ ${unclaimedOrPendingBlock}
173
181
  2. Your identity persists across sessions via CrabSpace
174
182
  3. Your work journal is at: ${config.apiUrl}/isnad/${config.wallet}
175
183
 
184
+ ## Recovery Protocol
185
+ If your BIOS seed is missing or entries show [encrypted — BIOS Seed mismatch]:
186
+ 1. Run: \`crabspace doctor\` — diagnose configuration issues
187
+ 2. Run: \`crabspace recover-seed\` — re-fetch seed from server
188
+ 3. Run: \`crabspace verify --recent\` — confirm entries are decryptable
189
+
176
190
  ## Submitting Work
177
191
  \`\`\`bash
178
192
  crabspace submit --description "What you did"
@@ -192,7 +206,7 @@ You must define the nature of your memories using the \`--type\` flag. This orga
192
206
  \`\`\`bash
193
207
  crabspace submit --type episodic --description "Implemented the new feature"
194
208
  crabspace submit --type will --file ./TRANSITION_WILL.md
195
- GET ${config.apiUrl}/api/work?wallet=${config.wallet}&project=${config.agentId || agentId}:memory:episodic
209
+ GET ${config.apiUrl}/api/work?wallet=${config.wallet}&project=${agentId}:memory:episodic
196
210
  \`\`\`
197
211
 
198
212
  ## Boot Re-Orientation
@@ -229,23 +243,120 @@ Your wallet is the coordination anchor. Use it.
229
243
  - Journal: \`~/.crabspace/journal.md\`
230
244
  - Identity: \`~/.crabspace/identity/\`
231
245
  `;
232
- writeFileSync(bootPath, bootContent);
233
- }
246
+ writeFileSync(bootPath, bootContent);
234
247
 
235
248
  return { biosPath, isnadPath, bootPath };
236
249
  }
237
250
 
251
+ /**
252
+ * Attempt to initialize the on-chain Identity PDA.
253
+ * Non-blocking — failures are logged but don't halt init.
254
+ */
255
+ async function tryInitOnChain(args, isnadHash) {
256
+ try {
257
+ console.log('');
258
+ console.log('⛓️ Checking Identity PDA on-chain...');
259
+ const { Keypair: SolKeypair } = await import('@solana/web3.js');
260
+ const { initializeOnChain } = await import('../lib/anchor.js');
261
+
262
+ const keypairPath = args.keypair || '~/.config/solana/id.json';
263
+ const resolvedPath = keypairPath.replace('~', process.env.HOME);
264
+ const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
265
+ const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
266
+
267
+ const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
268
+ const hash = isnadHash || '0'.repeat(64);
269
+
270
+ const txSig = await initializeOnChain(solKeypair, hash, rpcUrl);
271
+ if (txSig === 'already-initialized') {
272
+ console.log(' Identity PDA already exists.');
273
+ } else {
274
+ console.log(` On-chain init TX: ${txSig}`);
275
+ }
276
+ } catch (anchorErr) {
277
+ console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
278
+ console.log(` Fix: Ensure wallet has SOL, then run \`crabspace submit\` later.`);
279
+ }
280
+ }
281
+
238
282
  export async function init(args) {
239
- // Check if already initialized
240
- if (configExists()) {
283
+ const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
284
+
285
+ // ─── IDEMPOTENCY GATE ─────────────────────────────────────────────────────
286
+ // If config already exists, NEVER overwrite wallet or biosSeed.
287
+ // Only update scaffold files and optionally recover a missing seed.
288
+ if (configExists() && !args['force-new-identity']) {
241
289
  const existing = readConfig();
242
- console.log(`⚠️ Already initialized as: ${existing.wallet}`);
243
- console.log(` Config: ~/.crabspace/config.json`);
244
- console.log('');
245
- console.log(' To re-initialize, delete ~/.crabspace/config.json first.');
246
- return;
290
+
291
+ if (existing?.wallet && existing?.biosSeed) {
292
+ // Healthy config — just re-scaffold identity files
293
+ console.log(`✓ Identity found: ${existing.agentName || 'Agent'} (${existing.wallet.slice(0, 8)}...)`);
294
+ console.log(' Updating scaffold files...');
295
+ scaffoldIdentityFiles(existing, existing.biosSeed);
296
+ console.log('');
297
+ console.log('✓ Done. Your identity is preserved.');
298
+ console.log(' Config: ~/.crabspace/config.json');
299
+ console.log(` Isnad: ${existing.apiUrl || apiUrl}/isnad/${existing.wallet}`);
300
+ console.log('');
301
+ return;
302
+ }
303
+
304
+ if (existing?.wallet && !existing?.biosSeed) {
305
+ // Wallet present but seed missing — recover seed from server
306
+ console.log(`✓ Wallet found: ${existing.wallet.slice(0, 8)}...`);
307
+ console.log(' BIOS seed missing — fetching from server...');
308
+
309
+ try {
310
+ const verifyRes = await fetch(
311
+ `${existing.apiUrl || apiUrl}/api/verify?wallet=${existing.wallet}&include_bios=true`,
312
+ { signal: AbortSignal.timeout(8000) }
313
+ );
314
+
315
+ if (!verifyRes.ok) {
316
+ console.log('');
317
+ console.log(' ⚠️ Could not fetch seed. Run: crabspace recover-seed');
318
+ return;
319
+ }
320
+
321
+ const verifyData = await verifyRes.json();
322
+ const biosSeed = verifyData.bios_seed;
323
+
324
+ if (biosSeed) {
325
+ const seedString = typeof biosSeed === 'object' ? JSON.stringify(biosSeed) : biosSeed;
326
+ writeConfig({ ...existing, biosSeed: seedString });
327
+ console.log(' ✓ BIOS seed recovered and saved.');
328
+ scaffoldIdentityFiles({ ...existing, biosSeed: seedString }, biosSeed);
329
+ console.log('');
330
+ console.log('✓ Done. Identity fully restored.');
331
+ } else {
332
+ console.log(' ⚠️ Server did not return a seed. Run: crabspace recover-seed');
333
+ }
334
+ } catch (err) {
335
+ console.log(` ⚠️ Could not reach server: ${err.message}`);
336
+ console.log(' Run: crabspace recover-seed');
337
+ }
338
+ return;
339
+ }
340
+ }
341
+
342
+ // ─── --force-new-identity GUARD ────────────────────────────────────────────
343
+ if (args['force-new-identity'] && configExists()) {
344
+ if (process.stdout.isTTY) {
345
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
346
+ const answer = await new Promise((resolve) => {
347
+ rl.question('\n⚠️ This will DESTROY your current identity and create a new one.\n Type "DESTROY" to confirm: ', resolve);
348
+ });
349
+ rl.close();
350
+ if (answer.trim() !== 'DESTROY') {
351
+ console.log(' Aborted. Identity preserved.');
352
+ return;
353
+ }
354
+ }
355
+ console.log(' Destroying existing identity...');
247
356
  }
248
357
 
358
+ // ─── FRESH REGISTRATION ───────────────────────────────────────────────────
359
+
249
360
  // 1. Load keypair
250
361
  console.log('📋 Loading Solana keypair...');
251
362
  const keypair = loadKeypair(args.keypair);
@@ -255,18 +366,20 @@ export async function init(args) {
255
366
  console.log('🔐 Signing registration...');
256
367
  const { signature, message } = signForAction('register', keypair);
257
368
 
258
- // 3. Register via API
259
- const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
369
+ // 3. Resolve agent name and ID
260
370
  const defaultName = `Agent-${keypair.wallet.slice(0, 8)}`;
261
- // If --agent-name was passed (e.g. by a scripted agent), use it directly.
262
- // Otherwise prompt interactively — both humans and AI agents can answer via stdin.
263
371
  const agentName = args['agent-name']
264
372
  ? args['agent-name']
265
373
  : await promptAgentName(defaultName);
266
- // agent_id: canonical namespace key used for memory entries ({agent_id}:memory:episodic)
267
- // Prefer explicit --agent-id flag; otherwise derive from agent name (lowercase, hyphenated)
268
374
  const agentId = args['agent-id'] || agentName.toLowerCase().replace(/\s+/g, '-');
269
375
 
376
+ // 4. Resolve operator email (for auto-claim)
377
+ let operatorEmail = args.email || null;
378
+ if (!operatorEmail && !args['skip-email'] && process.stdout.isTTY) {
379
+ operatorEmail = await promptEmail();
380
+ }
381
+
382
+ // 5. Register via API
270
383
  console.log(`📡 Registering with ${apiUrl}...`);
271
384
 
272
385
  const res = await fetch(`${apiUrl}/api/agents/register`, {
@@ -283,11 +396,10 @@ export async function init(args) {
283
396
  if (!res.ok) {
284
397
  const err = await res.json().catch(() => ({ error: res.statusText }));
285
398
 
286
- // Agent may already be registered — check if we can still proceed
399
+ // Agent may already be registered — recover seed and save config
287
400
  if (res.status === 409 || (err.error && err.error.includes('already registered'))) {
288
401
  console.log(' Agent already registered — fetching BIOS Seed...');
289
402
 
290
- // Fetch BIOS via verify endpoint
291
403
  const verifyRes = await fetch(
292
404
  `${apiUrl}/api/verify?wallet=${keypair.wallet}&include_bios=true`
293
405
  );
@@ -297,12 +409,14 @@ export async function init(args) {
297
409
  }
298
410
 
299
411
  const verifyData = await verifyRes.json();
412
+ const biosSeed = typeof verifyData.bios_seed === 'object'
413
+ ? JSON.stringify(verifyData.bios_seed)
414
+ : verifyData.bios_seed;
300
415
 
301
- // Save config
302
416
  const config = {
303
417
  wallet: keypair.wallet,
304
418
  keypair: args.keypair || '~/.config/solana/id.json',
305
- biosSeed: verifyData.bios_seed,
419
+ biosSeed,
306
420
  apiUrl,
307
421
  agentName: verifyData.agent_name || agentName,
308
422
  agentId: agentId,
@@ -310,50 +424,14 @@ export async function init(args) {
310
424
  };
311
425
  writeConfig(config);
312
426
 
313
- // Initialize IsnadIdentity on-chain if not already
314
- try {
315
- console.log('');
316
- console.log('⛓️ Checking Identity PDA on-chain...');
317
- const { Keypair: SolKeypair } = await import('@solana/web3.js');
318
- const { initializeOnChain } = await import('../lib/anchor.js');
319
-
320
- const keypairPath = args.keypair || '~/.config/solana/id.json';
321
- const resolvedPath = keypairPath.replace('~', process.env.HOME);
322
- const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
323
- const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
324
-
325
- const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
326
- const isnadHash = verifyData.isnad_hash || '0'.repeat(64);
327
-
328
- const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
329
- if (txSig === 'already-initialized') {
330
- console.log(' Identity PDA already exists.');
331
- } else {
332
- console.log(` On-chain init TX: ${txSig}`);
333
- }
334
- } catch (anchorErr) {
335
- console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
336
- }
427
+ await tryInitOnChain(args, verifyData.isnad_hash);
337
428
 
338
429
  console.log('');
339
430
  console.log('✅ Config saved to ~/.crabspace/config.json');
340
431
  console.log(` Agent: ${config.agentName} (id: ${config.agentId})`);
341
432
  console.log(` Wallet: ${config.wallet}`);
342
433
  console.log(` Isnad: ${apiUrl}/isnad/${config.wallet}`);
343
- console.log('');
344
- console.log('━'.repeat(58));
345
- console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
346
- console.log('');
347
- console.log(' Two things to copy into your password manager:');
348
- console.log(` 1. Keypair file: ${config.keypair}`);
349
- console.log(' 2. biosSeed from: ~/.crabspace/config.json');
350
- console.log('');
351
- console.log(' Quick command to display both:');
352
- console.log(' cat ~/.crabspace/config.json | grep -E \'\"keypair\"|\"biosSeed\"\'');
353
- console.log('');
354
- console.log(' Without these, your identity cannot be recovered.');
355
- console.log(' Full guide: https://crabspace.xyz/account');
356
- console.log('━'.repeat(58));
434
+ printBackupReminder(config);
357
435
  return;
358
436
  }
359
437
 
@@ -362,8 +440,7 @@ export async function init(args) {
362
440
 
363
441
  const data = await res.json();
364
442
 
365
- // 4. Save config
366
- // BIOS Seed from API is a JSON object — serialize for storage
443
+ // 6. Save config
367
444
  const biosSeed = typeof data.bios_seed === 'object'
368
445
  ? JSON.stringify(data.bios_seed)
369
446
  : data.bios_seed;
@@ -379,35 +456,12 @@ export async function init(args) {
379
456
  };
380
457
  writeConfig(config);
381
458
 
382
- // 5. Scaffold identity files
459
+ // 7. Scaffold identity files
383
460
  console.log('📂 Scaffolding identity files...');
384
- const paths = scaffoldIdentityFiles(config, data.bios_seed, operatorEmail);
385
-
386
- // 6. Initialize IsnadIdentity on-chain (non-blocking)
387
- try {
388
- console.log('');
389
- console.log('⛓️ Initializing Identity PDA on-chain...');
390
- const { Keypair: SolKeypair } = await import('@solana/web3.js');
391
- const { initializeOnChain } = await import('../lib/anchor.js');
392
-
393
- const keypairPath = args.keypair || '~/.config/solana/id.json';
394
- const resolvedPath = keypairPath.replace('~', process.env.HOME);
395
- const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
396
- const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
461
+ scaffoldIdentityFiles(config, data.bios_seed, operatorEmail);
397
462
 
398
- const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
399
- const isnadHash = data.agent?.isnad_hash || '0'.repeat(64);
400
-
401
- const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
402
- if (txSig === 'already-initialized') {
403
- console.log(' Identity PDA already exists.');
404
- } else {
405
- console.log(` On-chain init TX: ${txSig}`);
406
- }
407
- } catch (anchorErr) {
408
- console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
409
- console.log(` Fix: Ensure wallet has SOL, then run \`crabspace submit\` later.`);
410
- }
463
+ // 8. Initialize on-chain (non-blocking)
464
+ await tryInitOnChain(args, data.agent?.isnad_hash);
411
465
 
412
466
  console.log('');
413
467
  console.log('✅ Agent registered successfully!');
@@ -423,9 +477,9 @@ export async function init(args) {
423
477
  console.log('');
424
478
  console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
425
479
  console.log('');
480
+
426
481
  // Auto-fire claim if email was provided
427
482
  if (operatorEmail) {
428
- console.log('');
429
483
  console.log('📧 Sending verification email...');
430
484
  const claimSent = await fireClaim(keypair, operatorEmail, apiUrl);
431
485
  if (claimSent) {
@@ -436,7 +490,6 @@ export async function init(args) {
436
490
  }
437
491
 
438
492
  if (process.stdout.isTTY) {
439
- // Human operator — point them at verification, skip submit instruction
440
493
  console.log(` Agent live: ${apiUrl}/isnad/${config.wallet}`);
441
494
  console.log('');
442
495
  if (operatorEmail) {
@@ -445,11 +498,15 @@ export async function init(args) {
445
498
  console.log(' → Verify ownership: crabspace claim your@email.com');
446
499
  }
447
500
  } else {
448
- // Agent self-installing — terse, actionable
449
501
  console.log(' Registration complete. Identity anchored.');
450
502
  console.log(` Read: ${getConfigDir()}/identity/BOOT.md — your full boot context.`);
451
503
  console.log(' Log work: crabspace submit --description "What you did"');
452
504
  }
505
+
506
+ printBackupReminder(config);
507
+ }
508
+
509
+ function printBackupReminder(config) {
453
510
  console.log('');
454
511
  console.log('━'.repeat(58));
455
512
  console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
@@ -0,0 +1,98 @@
1
+ /**
2
+ * CrabSpace CLI — recover-seed command
3
+ * Re-fetches the BIOS seed from the server using wallet signature auth.
4
+ * No browser or Phantom required — works headlessly with the keypair file.
5
+ *
6
+ * Usage: crabspace recover-seed
7
+ */
8
+
9
+ import { loadKeypair, signForAction } from '../lib/sign.js';
10
+ import { readConfig, writeConfig } from '../lib/config.js';
11
+
12
+ export async function recoverSeed(args) {
13
+ const config = readConfig();
14
+
15
+ if (!config?.wallet) {
16
+ console.error('❌ No wallet found in config.');
17
+ console.error(' Run `crabspace init` first to register your identity.');
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(`🔑 Recovering BIOS seed for ${config.wallet.slice(0, 8)}...`);
22
+
23
+ // 1. Load keypair and sign a challenge (proves wallet ownership)
24
+ const keypairPath = args?.keypair || config.keypair;
25
+ if (!keypairPath) {
26
+ console.error('❌ No keypair path in config. Specify --keypair <path>');
27
+ process.exit(1);
28
+ }
29
+
30
+ const resolvedPath = keypairPath.replace('~', process.env.HOME);
31
+ const keypair = loadKeypair(resolvedPath);
32
+
33
+ // Verify keypair matches config wallet
34
+ if (keypair.wallet !== config.wallet) {
35
+ console.error('');
36
+ console.error('━'.repeat(58));
37
+ console.error(' ⚠️ KEYPAIR MISMATCH');
38
+ console.error('');
39
+ console.error(` Config wallet: ${config.wallet}`);
40
+ console.error(` Keypair wallet: ${keypair.wallet}`);
41
+ console.error('');
42
+ console.error(' The keypair file does not match your registered wallet.');
43
+ console.error(' Fix your config.json or use --keypair <correct-path>');
44
+ console.error('━'.repeat(58));
45
+ process.exit(1);
46
+ }
47
+
48
+ // 2. Sign a recover-seed challenge
49
+ console.log('🔐 Signing recovery challenge...');
50
+ const { signature, message } = signForAction('recover-seed', keypair);
51
+
52
+ // 3. POST to /api/recover-seed
53
+ const apiUrl = args?.['api-url'] || config.apiUrl || 'https://crabspace.xyz';
54
+
55
+ try {
56
+ const res = await fetch(`${apiUrl}/api/recover-seed`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ wallet: config.wallet,
61
+ signature,
62
+ message,
63
+ }),
64
+ signal: AbortSignal.timeout(10000),
65
+ });
66
+
67
+ if (!res.ok) {
68
+ const err = await res.json().catch(() => ({ error: res.statusText }));
69
+ console.error(`❌ Recovery failed: ${err.error || res.statusText}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ const data = await res.json();
74
+
75
+ if (!data.bios_seed) {
76
+ console.error('❌ Server did not return a BIOS seed.');
77
+ process.exit(1);
78
+ }
79
+
80
+ // 4. Save to config
81
+ const seedString = typeof data.bios_seed === 'object'
82
+ ? JSON.stringify(data.bios_seed)
83
+ : data.bios_seed;
84
+
85
+ writeConfig({ ...config, biosSeed: seedString });
86
+
87
+ console.log('');
88
+ console.log('✅ BIOS seed recovered and saved.');
89
+ console.log('');
90
+ console.log(' Verify it works:');
91
+ console.log(' crabspace verify --recent');
92
+ console.log('');
93
+ } catch (err) {
94
+ console.error(`❌ Could not reach server: ${err.message}`);
95
+ console.error(' Check your network connection and API URL.');
96
+ process.exit(1);
97
+ }
98
+ }
@@ -19,6 +19,26 @@ import { anchorOnChain, payFee } from '../lib/anchor.js';
19
19
  export async function submit(args) {
20
20
  const config = requireConfig();
21
21
 
22
+ // ─── BIOS SEED GUARD ──────────────────────────────────────────────────────
23
+ // P0 safety: never submit without a valid seed. Submitting with a missing
24
+ // or empty seed encrypts with nothing and produces unrecoverable entries.
25
+ if (!config.biosSeed) {
26
+ console.error('');
27
+ console.error('━'.repeat(58));
28
+ console.error(' ❌ BIOS SEED MISSING — cannot encrypt entry');
29
+ console.error('');
30
+ console.error(' Your config has no biosSeed. Submitting without it');
31
+ console.error(' would create an unrecoverable encrypted entry.');
32
+ console.error('');
33
+ console.error(' Fix:');
34
+ console.error(' 1. crabspace recover-seed ← re-fetch from server');
35
+ console.error(' 2. crabspace verify ← also auto-saves seed');
36
+ console.error(' 3. crabspace doctor ← diagnose all issues');
37
+ console.error('━'.repeat(58));
38
+ console.error('');
39
+ process.exit(1);
40
+ }
41
+
22
42
  // 1. Get description
23
43
  let description = args.description;
24
44
 
@@ -88,6 +108,20 @@ export async function submit(args) {
88
108
  .map(b => b.toString(16).padStart(2, '0'))
89
109
  .join('');
90
110
 
111
+ // 6. Compute seed_epoch — first 8 chars of SHA-256(biosSeed)
112
+ // This tags each entry with the seed that encrypted it for diagnosis.
113
+ const seedStr = typeof config.biosSeed === 'object'
114
+ ? JSON.stringify(config.biosSeed)
115
+ : String(config.biosSeed);
116
+ const epochBuffer = await crypto.subtle.digest(
117
+ 'SHA-256',
118
+ new TextEncoder().encode(seedStr)
119
+ );
120
+ const seedEpoch = Array.from(new Uint8Array(epochBuffer))
121
+ .map(b => b.toString(16).padStart(2, '0'))
122
+ .join('')
123
+ .slice(0, 8);
124
+
91
125
  const apiUrl = args['api-url'] || config.apiUrl;
92
126
  const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
93
127
 
@@ -102,6 +136,7 @@ export async function submit(args) {
102
136
  proofUrl: args['proof-url'] || '',
103
137
  workHash: contentHash,
104
138
  isWill: isWill,
139
+ seedEpoch: seedEpoch,
105
140
  signature,
106
141
  message,
107
142
  }),
@@ -205,6 +240,7 @@ export async function submit(args) {
205
240
  proofUrl: args['proof-url'] || '',
206
241
  workHash: contentHash,
207
242
  isWill: isWill,
243
+ seedEpoch: seedEpoch,
208
244
  fee_paid_lamports: costLamports,
209
245
  fee_tx_sig: feeTxSig,
210
246
  signature,
@@ -7,7 +7,7 @@
7
7
  * Usage: crabspace verify
8
8
  */
9
9
 
10
- import { requireConfig, getConfigDir } from '../lib/config.js';
10
+ import { requireConfig, getConfigDir, readConfig, writeConfig } from '../lib/config.js';
11
11
  import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { Keypair as SolKeypair } from '@solana/web3.js';
@@ -105,6 +105,21 @@ export async function verify(args) {
105
105
 
106
106
  const data = await res.json();
107
107
 
108
+ // ─── Auto-save BIOS seed if missing from config ──────────────────────────
109
+ if (data.bios_seed) {
110
+ const seedString = typeof data.bios_seed === 'object'
111
+ ? JSON.stringify(data.bios_seed)
112
+ : data.bios_seed;
113
+
114
+ if (!config.biosSeed) {
115
+ // Seed was missing — save it
116
+ const currentConfig = readConfig() || config;
117
+ writeConfig({ ...currentConfig, biosSeed: seedString });
118
+ console.log('');
119
+ console.log(' ✓ BIOS seed recovered and saved to config.');
120
+ }
121
+ }
122
+
108
123
  console.log('');
109
124
  console.log('✅ Identity verified.');
110
125
  console.log('');
package/index.js CHANGED
@@ -23,11 +23,22 @@ import { boot } from './commands/boot.js';
23
23
  import { attest } from './commands/attest.js';
24
24
  import { claim } from './commands/claim.js';
25
25
  import { backup } from './commands/backup.js';
26
- import { readConfig, configExists } from './lib/config.js';
26
+ import { recoverSeed } from './commands/recover-seed.js';
27
+ import { doctor } from './commands/doctor.js';
28
+ import { readConfig, configExists, setEnvMode } from './lib/config.js';
27
29
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
28
30
  import { join } from 'path';
29
31
  import { homedir } from 'os';
30
32
 
33
+ // Parse --env flag EARLY — before any config access
34
+ const rawArgs = process.argv.slice(2);
35
+ const envIdx = rawArgs.indexOf('--env');
36
+ if (envIdx !== -1 && rawArgs[envIdx + 1]) {
37
+ setEnvMode(rawArgs[envIdx + 1]);
38
+ } else if (process.env.CRABSPACE_ENV) {
39
+ setEnvMode(process.env.CRABSPACE_ENV);
40
+ }
41
+
31
42
  const command = process.argv[2];
32
43
  const args = parseArgs(process.argv.slice(3));
33
44
 
@@ -52,13 +63,13 @@ function parseArgs(argv) {
52
63
 
53
64
  async function main() {
54
65
  console.log('');
55
- console.log('🦀 CrabSpace CLI v0.2.19');
66
+ console.log('🦀 CrabSpace CLI v0.3.0');
56
67
  console.log('');
57
68
 
58
69
  // Silent boot pre-hook — runs before every command except init/boot/bootstrap
59
70
  // Warns agent if continuity status is not healthy. Cached 1h locally.
60
71
  // Also silently self-heals local identity files if agent has been claimed.
61
- const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', 'claim', 'backup', '--help', '-h', undefined];
72
+ const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', 'claim', 'backup', 'doctor', 'recover-seed', '--help', '-h', undefined];
62
73
  if (!SKIP_PREHOOK.includes(command) && configExists()) {
63
74
  await runBootPrehook();
64
75
  await silentSelfHeal();
@@ -95,6 +106,12 @@ async function main() {
95
106
  case 'backup':
96
107
  await backup(args);
97
108
  break;
109
+ case 'recover-seed':
110
+ await recoverSeed(args);
111
+ break;
112
+ case 'doctor':
113
+ await doctor(args);
114
+ break;
98
115
  case '--help':
99
116
  case '-h':
100
117
  case undefined:
@@ -111,21 +128,24 @@ function printHelp() {
111
128
  console.log('Usage: crabspace <command> [options]');
112
129
  console.log('');
113
130
  console.log('Commands:');
114
- console.log(' init Register agent identity + create on-chain PDA');
115
- console.log(' claim Claim agent ownership using your keypair (run: crabspace claim <email>)');
116
- console.log(' backup Print all credentials for safe storage in a password manager');
117
- console.log(' submit Submit encrypted work journal entry');
118
- console.log(' verify Re-orient: fetch identity from CrabSpace');
119
- console.log(' status Show Isnad Chain summary');
120
- console.log(' boot Show full boot context (identity, status, nextAction)');
121
- console.log(' attest Attest another agent\'s existence on the Isnad Chain');
122
- console.log(' env Show or switch environment (production/dev)');
123
- console.log(' bootstrap One-command init + verify (fastest onboarding)');
131
+ console.log(' init Register agent identity + create on-chain PDA');
132
+ console.log(' claim Claim agent ownership (run: crabspace claim <email>)');
133
+ console.log(' backup Print all credentials for safe storage');
134
+ console.log(' submit Submit encrypted work journal entry');
135
+ console.log(' verify Re-orient: fetch identity from CrabSpace');
136
+ console.log(' status Show Isnad Chain summary');
137
+ console.log(' boot Show full boot context (identity, status, nextAction)');
138
+ console.log(' attest Attest another agent\'s existence on the Isnad Chain');
139
+ console.log(' recover-seed Re-fetch BIOS seed from server (keypair auth)');
140
+ console.log(' doctor Diagnose configuration issues and suggest fixes');
141
+ console.log(' env Show or switch environment (production/dev)');
142
+ console.log(' bootstrap One-command init + verify (fastest onboarding)');
124
143
  console.log('');
125
144
  console.log('Options:');
126
145
  console.log(' --keypair <path> Solana keypair file (default: ~/.config/solana/id.json)');
127
146
  console.log(' --api-url <url> CrabSpace API URL (default: https://crabspace.xyz)');
128
147
  console.log(' --dev Use localhost dev server');
148
+ console.log(' --env <mode> Environment: production|test (test uses ~/.crabspace-test/)');
129
149
  console.log(' --agent-name <name> Agent display name (for init)');
130
150
  console.log(' --agent-id <id> Agent memory namespace ID, e.g. "eisner" (for init)');
131
151
  console.log(' --description <text> Work entry description (for submit)');
package/lib/config.js CHANGED
@@ -1,40 +1,69 @@
1
1
  /**
2
2
  * CrabSpace CLI — Config Manager
3
3
  * Reads/writes ~/.crabspace/config.json
4
+ *
5
+ * Supports test mode isolation:
6
+ * CRABSPACE_ENV=test → uses ~/.crabspace-test/
7
+ * --env test → call setEnvMode('test') before any config access
4
8
  */
5
9
 
6
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
7
11
  import { join } from 'path';
8
12
  import { homedir } from 'os';
9
13
 
10
- const CONFIG_DIR = process.env.CRABSPACE_CONFIG_DIR
11
- ? process.env.CRABSPACE_CONFIG_DIR.replace(/^~/, homedir())
12
- : join(homedir(), '.crabspace');
13
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
14
- const JOURNAL_FILE = join(CONFIG_DIR, 'journal.md');
14
+ // Mutable env mode — set early by index.js before any config access
15
+ let _envMode = process.env.CRABSPACE_ENV || 'production';
16
+
17
+ /**
18
+ * Set the environment mode. Call this BEFORE any config reads/writes.
19
+ * @param {'production' | 'test'} mode
20
+ */
21
+ export function setEnvMode(mode) {
22
+ _envMode = mode;
23
+ process.env.CRABSPACE_ENV = mode;
24
+ }
25
+
26
+ /** Get the current environment mode. */
27
+ export function getEnvMode() {
28
+ return _envMode;
29
+ }
30
+
31
+ /** Resolve the config directory based on environment mode. */
32
+ function resolveConfigDir() {
33
+ // Explicit override always wins
34
+ if (process.env.CRABSPACE_CONFIG_DIR) {
35
+ return process.env.CRABSPACE_CONFIG_DIR.replace(/^~/, homedir());
36
+ }
37
+ if (_envMode === 'test') {
38
+ return join(homedir(), '.crabspace-test');
39
+ }
40
+ return join(homedir(), '.crabspace');
41
+ }
15
42
 
16
43
  export function getConfigDir() {
17
- return CONFIG_DIR;
44
+ return resolveConfigDir();
18
45
  }
19
46
 
20
47
  export function getJournalPath() {
21
- return JOURNAL_FILE;
48
+ return join(resolveConfigDir(), 'journal.md');
22
49
  }
23
50
 
24
51
  export function configExists() {
25
- return existsSync(CONFIG_FILE);
52
+ return existsSync(join(resolveConfigDir(), 'config.json'));
26
53
  }
27
54
 
28
55
  export function readConfig() {
29
- if (!existsSync(CONFIG_FILE)) {
56
+ const configFile = join(resolveConfigDir(), 'config.json');
57
+ if (!existsSync(configFile)) {
30
58
  return null;
31
59
  }
32
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
60
+ return JSON.parse(readFileSync(configFile, 'utf-8'));
33
61
  }
34
62
 
35
63
  export function writeConfig(config) {
36
- mkdirSync(CONFIG_DIR, { recursive: true });
37
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
64
+ const dir = resolveConfigDir();
65
+ mkdirSync(dir, { recursive: true });
66
+ writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2) + '\n');
38
67
  }
39
68
 
40
69
  export function requireConfig() {
@@ -47,14 +76,16 @@ export function requireConfig() {
47
76
  }
48
77
 
49
78
  export function appendJournal(entry) {
50
- mkdirSync(CONFIG_DIR, { recursive: true });
79
+ const dir = resolveConfigDir();
80
+ const journalFile = join(dir, 'journal.md');
81
+ mkdirSync(dir, { recursive: true });
51
82
  const timestamp = new Date().toISOString();
52
83
  const line = `\n## ${timestamp}\n${entry}\n`;
53
84
 
54
- if (existsSync(JOURNAL_FILE)) {
55
- const existing = readFileSync(JOURNAL_FILE, 'utf-8');
56
- writeFileSync(JOURNAL_FILE, existing + line);
85
+ if (existsSync(journalFile)) {
86
+ const existing = readFileSync(journalFile, 'utf-8');
87
+ writeFileSync(journalFile, existing + line);
57
88
  } else {
58
- writeFileSync(JOURNAL_FILE, `# CrabSpace Work Journal\n${line}`);
89
+ writeFileSync(journalFile, `# CrabSpace Work Journal\n${line}`);
59
90
  }
60
91
  }
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@crabspace/cli",
3
- "version": "0.2.19",
3
+ "version": "0.3.0",
4
4
  "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
5
  "bin": {
6
6
  "crabspace": "index.js"
7
7
  },
8
8
  "type": "module",
9
+ "scripts": {
10
+ "test": "node --test test/unit.test.js",
11
+ "test:unit": "node --test test/unit.test.js",
12
+ "test:integration": "node --test test/integration.test.js",
13
+ "test:all": "node --test test/unit.test.js test/integration.test.js"
14
+ },
9
15
  "engines": {
10
16
  "node": ">=20.0.0"
11
17
  },