@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.
- package/commands/doctor.js +170 -0
- package/commands/init.js +159 -95
- package/commands/recover-seed.js +98 -0
- package/commands/status.js +21 -0
- package/commands/submit.js +36 -0
- package/commands/verify.js +37 -1
- package/index.js +33 -13
- package/lib/config.js +48 -17
- package/package.json +7 -1
|
@@ -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
|
-
:
|
|
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
|
-
|
|
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=${
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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.
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
459
|
+
// 7. Scaffold identity files
|
|
376
460
|
console.log('📂 Scaffolding identity files...');
|
|
377
|
-
|
|
461
|
+
scaffoldIdentityFiles(config, data.bios_seed, operatorEmail);
|
|
378
462
|
|
|
379
|
-
//
|
|
380
|
-
|
|
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
|
+
}
|
package/commands/status.js
CHANGED
|
@@ -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
|
}
|
package/commands/submit.js
CHANGED
|
@@ -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,
|
package/commands/verify.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
|
115
|
-
console.log(' claim
|
|
116
|
-
console.log(' backup
|
|
117
|
-
console.log(' submit
|
|
118
|
-
console.log(' verify
|
|
119
|
-
console.log(' status
|
|
120
|
-
console.log(' boot
|
|
121
|
-
console.log(' attest
|
|
122
|
-
console.log('
|
|
123
|
-
console.log('
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
44
|
+
return resolveConfigDir();
|
|
18
45
|
}
|
|
19
46
|
|
|
20
47
|
export function getJournalPath() {
|
|
21
|
-
return
|
|
48
|
+
return join(resolveConfigDir(), 'journal.md');
|
|
22
49
|
}
|
|
23
50
|
|
|
24
51
|
export function configExists() {
|
|
25
|
-
return existsSync(
|
|
52
|
+
return existsSync(join(resolveConfigDir(), 'config.json'));
|
|
26
53
|
}
|
|
27
54
|
|
|
28
55
|
export function readConfig() {
|
|
29
|
-
|
|
56
|
+
const configFile = join(resolveConfigDir(), 'config.json');
|
|
57
|
+
if (!existsSync(configFile)) {
|
|
30
58
|
return null;
|
|
31
59
|
}
|
|
32
|
-
return JSON.parse(readFileSync(
|
|
60
|
+
return JSON.parse(readFileSync(configFile, 'utf-8'));
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
export function writeConfig(config) {
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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(
|
|
55
|
-
const existing = readFileSync(
|
|
56
|
-
writeFileSync(
|
|
85
|
+
if (existsSync(journalFile)) {
|
|
86
|
+
const existing = readFileSync(journalFile, 'utf-8');
|
|
87
|
+
writeFileSync(journalFile, existing + line);
|
|
57
88
|
} else {
|
|
58
|
-
writeFileSync(
|
|
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.
|
|
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
|
},
|