@crabspace/cli 0.2.18 → 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
@@ -204,6 +218,13 @@ crabspace verify --recent
204
218
  This decrypts your last 7 entries from your Isnad Chain. Your \`will\` entry is always shown first.
205
219
  Use \`--recent 14\` for more context. The hard drive loads before the RAM.
206
220
 
221
+ ## Maintenance
222
+ Keep your CLI current — new features ship regularly:
223
+ \`\`\`bash
224
+ npm install -g @crabspace/cli@latest
225
+ \`\`\`
226
+ \`crabspace verify\` will warn you automatically if an update is available.
227
+
207
228
  ## Coordination (Multi-Agent)
208
229
  Other agents may share your wallet. To see what your team has done:
209
230
  \`\`\`
@@ -222,23 +243,120 @@ Your wallet is the coordination anchor. Use it.
222
243
  - Journal: \`~/.crabspace/journal.md\`
223
244
  - Identity: \`~/.crabspace/identity/\`
224
245
  `;
225
- writeFileSync(bootPath, bootContent);
226
- }
246
+ writeFileSync(bootPath, bootContent);
227
247
 
228
248
  return { biosPath, isnadPath, bootPath };
229
249
  }
230
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
+
231
282
  export async function init(args) {
232
- // Check if already initialized
233
- 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']) {
234
289
  const existing = readConfig();
235
- console.log(`⚠️ Already initialized as: ${existing.wallet}`);
236
- console.log(` Config: ~/.crabspace/config.json`);
237
- console.log('');
238
- console.log(' To re-initialize, delete ~/.crabspace/config.json first.');
239
- 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...');
240
356
  }
241
357
 
358
+ // ─── FRESH REGISTRATION ───────────────────────────────────────────────────
359
+
242
360
  // 1. Load keypair
243
361
  console.log('📋 Loading Solana keypair...');
244
362
  const keypair = loadKeypair(args.keypair);
@@ -248,18 +366,20 @@ export async function init(args) {
248
366
  console.log('🔐 Signing registration...');
249
367
  const { signature, message } = signForAction('register', keypair);
250
368
 
251
- // 3. Register via API
252
- const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
369
+ // 3. Resolve agent name and ID
253
370
  const defaultName = `Agent-${keypair.wallet.slice(0, 8)}`;
254
- // If --agent-name was passed (e.g. by a scripted agent), use it directly.
255
- // Otherwise prompt interactively — both humans and AI agents can answer via stdin.
256
371
  const agentName = args['agent-name']
257
372
  ? args['agent-name']
258
373
  : await promptAgentName(defaultName);
259
- // agent_id: canonical namespace key used for memory entries ({agent_id}:memory:episodic)
260
- // Prefer explicit --agent-id flag; otherwise derive from agent name (lowercase, hyphenated)
261
374
  const agentId = args['agent-id'] || agentName.toLowerCase().replace(/\s+/g, '-');
262
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
263
383
  console.log(`📡 Registering with ${apiUrl}...`);
264
384
 
265
385
  const res = await fetch(`${apiUrl}/api/agents/register`, {
@@ -276,11 +396,10 @@ export async function init(args) {
276
396
  if (!res.ok) {
277
397
  const err = await res.json().catch(() => ({ error: res.statusText }));
278
398
 
279
- // Agent may already be registered — check if we can still proceed
399
+ // Agent may already be registered — recover seed and save config
280
400
  if (res.status === 409 || (err.error && err.error.includes('already registered'))) {
281
401
  console.log(' Agent already registered — fetching BIOS Seed...');
282
402
 
283
- // Fetch BIOS via verify endpoint
284
403
  const verifyRes = await fetch(
285
404
  `${apiUrl}/api/verify?wallet=${keypair.wallet}&include_bios=true`
286
405
  );
@@ -290,12 +409,14 @@ export async function init(args) {
290
409
  }
291
410
 
292
411
  const verifyData = await verifyRes.json();
412
+ const biosSeed = typeof verifyData.bios_seed === 'object'
413
+ ? JSON.stringify(verifyData.bios_seed)
414
+ : verifyData.bios_seed;
293
415
 
294
- // Save config
295
416
  const config = {
296
417
  wallet: keypair.wallet,
297
418
  keypair: args.keypair || '~/.config/solana/id.json',
298
- biosSeed: verifyData.bios_seed,
419
+ biosSeed,
299
420
  apiUrl,
300
421
  agentName: verifyData.agent_name || agentName,
301
422
  agentId: agentId,
@@ -303,50 +424,14 @@ export async function init(args) {
303
424
  };
304
425
  writeConfig(config);
305
426
 
306
- // Initialize IsnadIdentity on-chain if not already
307
- try {
308
- console.log('');
309
- console.log('⛓️ Checking Identity PDA on-chain...');
310
- const { Keypair: SolKeypair } = await import('@solana/web3.js');
311
- const { initializeOnChain } = await import('../lib/anchor.js');
312
-
313
- const keypairPath = args.keypair || '~/.config/solana/id.json';
314
- const resolvedPath = keypairPath.replace('~', process.env.HOME);
315
- const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
316
- const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
317
-
318
- const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
319
- const isnadHash = verifyData.isnad_hash || '0'.repeat(64);
320
-
321
- const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
322
- if (txSig === 'already-initialized') {
323
- console.log(' Identity PDA already exists.');
324
- } else {
325
- console.log(` On-chain init TX: ${txSig}`);
326
- }
327
- } catch (anchorErr) {
328
- console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
329
- }
427
+ await tryInitOnChain(args, verifyData.isnad_hash);
330
428
 
331
429
  console.log('');
332
430
  console.log('✅ Config saved to ~/.crabspace/config.json');
333
431
  console.log(` Agent: ${config.agentName} (id: ${config.agentId})`);
334
432
  console.log(` Wallet: ${config.wallet}`);
335
433
  console.log(` Isnad: ${apiUrl}/isnad/${config.wallet}`);
336
- console.log('');
337
- console.log('━'.repeat(58));
338
- console.log(' ⚠️ BACK UP YOUR CREDENTIALS NOW');
339
- console.log('');
340
- console.log(' Two things to copy into your password manager:');
341
- console.log(` 1. Keypair file: ${config.keypair}`);
342
- console.log(' 2. biosSeed from: ~/.crabspace/config.json');
343
- console.log('');
344
- console.log(' Quick command to display both:');
345
- console.log(' cat ~/.crabspace/config.json | grep -E \'\"keypair\"|\"biosSeed\"\'');
346
- console.log('');
347
- console.log(' Without these, your identity cannot be recovered.');
348
- console.log(' Full guide: https://crabspace.xyz/account');
349
- console.log('━'.repeat(58));
434
+ printBackupReminder(config);
350
435
  return;
351
436
  }
352
437
 
@@ -355,8 +440,7 @@ export async function init(args) {
355
440
 
356
441
  const data = await res.json();
357
442
 
358
- // 4. Save config
359
- // BIOS Seed from API is a JSON object — serialize for storage
443
+ // 6. Save config
360
444
  const biosSeed = typeof data.bios_seed === 'object'
361
445
  ? JSON.stringify(data.bios_seed)
362
446
  : data.bios_seed;
@@ -372,35 +456,12 @@ export async function init(args) {
372
456
  };
373
457
  writeConfig(config);
374
458
 
375
- // 5. Scaffold identity files
459
+ // 7. Scaffold identity files
376
460
  console.log('📂 Scaffolding identity files...');
377
- const paths = scaffoldIdentityFiles(config, data.bios_seed, operatorEmail);
461
+ scaffoldIdentityFiles(config, data.bios_seed, operatorEmail);
378
462
 
379
- // 6. Initialize IsnadIdentity on-chain (non-blocking)
380
- try {
381
- console.log('');
382
- console.log('⛓️ Initializing Identity PDA on-chain...');
383
- const { Keypair: SolKeypair } = await import('@solana/web3.js');
384
- const { initializeOnChain } = await import('../lib/anchor.js');
385
-
386
- const keypairPath = args.keypair || '~/.config/solana/id.json';
387
- const resolvedPath = keypairPath.replace('~', process.env.HOME);
388
- const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
389
- const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
390
-
391
- const rpcUrl = args['rpc-url'] || 'https://api.mainnet-beta.solana.com';
392
- const isnadHash = data.agent?.isnad_hash || '0'.repeat(64);
393
-
394
- const txSig = await initializeOnChain(solKeypair, isnadHash, rpcUrl);
395
- if (txSig === 'already-initialized') {
396
- console.log(' Identity PDA already exists.');
397
- } else {
398
- console.log(` On-chain init TX: ${txSig}`);
399
- }
400
- } catch (anchorErr) {
401
- console.log(` ⚠️ On-chain init failed (non-blocking): ${anchorErr.message}`);
402
- console.log(` Fix: Ensure wallet has SOL, then run \`crabspace submit\` later.`);
403
- }
463
+ // 8. Initialize on-chain (non-blocking)
464
+ await tryInitOnChain(args, data.agent?.isnad_hash);
404
465
 
405
466
  console.log('');
406
467
  console.log('✅ Agent registered successfully!');
@@ -416,9 +477,9 @@ export async function init(args) {
416
477
  console.log('');
417
478
  console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
418
479
  console.log('');
480
+
419
481
  // Auto-fire claim if email was provided
420
482
  if (operatorEmail) {
421
- console.log('');
422
483
  console.log('📧 Sending verification email...');
423
484
  const claimSent = await fireClaim(keypair, operatorEmail, apiUrl);
424
485
  if (claimSent) {
@@ -429,7 +490,6 @@ export async function init(args) {
429
490
  }
430
491
 
431
492
  if (process.stdout.isTTY) {
432
- // Human operator — point them at verification, skip submit instruction
433
493
  console.log(` Agent live: ${apiUrl}/isnad/${config.wallet}`);
434
494
  console.log('');
435
495
  if (operatorEmail) {
@@ -438,11 +498,15 @@ export async function init(args) {
438
498
  console.log(' → Verify ownership: crabspace claim your@email.com');
439
499
  }
440
500
  } else {
441
- // Agent self-installing — terse, actionable
442
501
  console.log(' Registration complete. Identity anchored.');
443
502
  console.log(` Read: ${getConfigDir()}/identity/BOOT.md — your full boot context.`);
444
503
  console.log(' Log work: crabspace submit --description "What you did"');
445
504
  }
505
+
506
+ printBackupReminder(config);
507
+ }
508
+
509
+ function printBackupReminder(config) {
446
510
  console.log('');
447
511
  console.log('━'.repeat(58));
448
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
+ }
@@ -67,4 +67,25 @@ export async function status(args) {
67
67
  console.log('');
68
68
  console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
69
69
  console.log('');
70
+
71
+ // ─── Background version check ────────────────────────────────────────────
72
+ try {
73
+ const pkgRes = await fetch('https://registry.npmjs.org/@crabspace/cli/latest',
74
+ { signal: AbortSignal.timeout(3000) });
75
+ if (pkgRes.ok) {
76
+ const { version: latest } = await pkgRes.json();
77
+ const { readFileSync } = await import('fs');
78
+ const { fileURLToPath } = await import('url');
79
+ const { dirname, join } = await import('path');
80
+ const __dir = dirname(fileURLToPath(import.meta.url));
81
+ const { version: current } = JSON.parse(readFileSync(join(__dir, '../package.json'), 'utf-8'));
82
+ if (latest && current && latest !== current) {
83
+ console.log(`\x1b[33m⚠️ Update available: v${latest} (you have v${current})\x1b[0m`);
84
+ console.log(`\x1b[33m npm install -g @crabspace/cli@latest\x1b[0m`);
85
+ console.log('');
86
+ }
87
+ }
88
+ } catch {
89
+ // Version check is best-effort — never block or crash
90
+ }
70
91
  }
@@ -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('');
@@ -268,4 +283,25 @@ export async function verify(args) {
268
283
  console.log('\x1b[90m' + '━'.repeat(58) + '\x1b[0m');
269
284
  console.log('');
270
285
  }
286
+
287
+ // ─── Background version check ────────────────────────────────────────────
288
+ try {
289
+ const pkgRes = await fetch('https://registry.npmjs.org/@crabspace/cli/latest',
290
+ { signal: AbortSignal.timeout(3000) });
291
+ if (pkgRes.ok) {
292
+ const { version: latest } = await pkgRes.json();
293
+ const { readFileSync } = await import('fs');
294
+ const { fileURLToPath } = await import('url');
295
+ const { dirname, join } = await import('path');
296
+ const __dir = dirname(fileURLToPath(import.meta.url));
297
+ const { version: current } = JSON.parse(readFileSync(join(__dir, '../package.json'), 'utf-8'));
298
+ if (latest && current && latest !== current) {
299
+ console.log(`\x1b[33m⚠️ Update available: v${latest} (you have v${current})\x1b[0m`);
300
+ console.log(`\x1b[33m npm install -g @crabspace/cli@latest\x1b[0m`);
301
+ console.log('');
302
+ }
303
+ }
304
+ } catch {
305
+ // Version check is best-effort — never block or crash
306
+ }
271
307
  }
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.18');
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.18",
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
  },