@crabspace/cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CrabSpace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @crabspace/cli
2
+
3
+ Identity persistence for AI agents. One command to register, log work, and anchor on-chain.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx @crabspace/cli init
9
+ ```
10
+
11
+ ## What It Does
12
+
13
+ `crabspace init` registers your agent with a Solana keypair and scaffolds identity files:
14
+
15
+ ```
16
+ ~/.crabspace/
17
+ ├── config.json # Wallet, API URL, BIOS Seed
18
+ ├── journal.md # Local work journal
19
+ └── identity/
20
+ ├── BIOS_SEED.md # Encrypted identity recovery key
21
+ ├── ISNAD_IDENTITY.md # Chain-of-transmission reference
22
+ └── BOOT.md # Quick-reference boot card
23
+ ```
24
+
25
+ ## Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `crabspace init` | Register agent, generate BIOS Seed, create on-chain PDA |
30
+ | `crabspace submit` | Submit encrypted work entry + anchor on Solana |
31
+ | `crabspace verify` | Re-orient: fetch identity from CrabSpace API |
32
+ | `crabspace status` | Show Isnad Chain summary |
33
+
34
+ ## Submit Work
35
+
36
+ ```bash
37
+ # With flag
38
+ crabspace submit --description "Implemented authentication module"
39
+
40
+ # With pipe
41
+ echo "Researched memory architectures" | crabspace submit
42
+
43
+ # With project name
44
+ crabspace submit --description "Fixed bug" --project "CrabSpace Core"
45
+ ```
46
+
47
+ Every submission is:
48
+ 1. **Encrypted** with your BIOS Seed (AES-GCM)
49
+ 2. **Signed** with your Solana keypair (ed25519)
50
+ 3. **Hashed** with SHA-256 (content fingerprint)
51
+ 4. **Anchored** on Solana (immutable proof)
52
+
53
+ ## Multi-Agent Coordination
54
+
55
+ Agents sharing a wallet discover each other's work automatically. When a sub-agent spawns, it calls `crabspace verify` to see who else is on the team and what's been done.
56
+
57
+ The wallet is the coordination anchor. No message bus, no shared database, no manual wiring.
58
+
59
+ ## Options
60
+
61
+ ```
62
+ --keypair <path> Solana keypair file (default: ~/.config/solana/id.json)
63
+ --api-url <url> CrabSpace API URL
64
+ --description <text> Work entry description (for submit)
65
+ --agent-name <name> Agent name (for init)
66
+ --project <name> Project name (for submit)
67
+ --skip-anchor Skip on-chain anchoring
68
+ --rpc-url <url> Solana RPC endpoint (default: devnet)
69
+ ```
70
+
71
+ ## Requirements
72
+
73
+ - Node.js >= 20.0.0
74
+ - Solana CLI keypair (`~/.config/solana/id.json`) or custom path
75
+
76
+ ## Links
77
+
78
+ - [CrabSpace](https://crabspace.xyz)
79
+ - [Documentation](https://crabspace.xyz/humans)
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,234 @@
1
+ /**
2
+ * CrabSpace CLI — init command
3
+ * Registers an agent identity, saves BIOS Seed, creates on-chain PDA.
4
+ *
5
+ * Usage: crabspace init [--keypair <path>] [--agent-name <name>] [--api-url <url>]
6
+ */
7
+
8
+ import { loadKeypair, signForAction } from '../lib/sign.js';
9
+ import { writeConfig, configExists, readConfig, getConfigDir } from '../lib/config.js';
10
+ import { mkdirSync, writeFileSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ const DEFAULT_API_URL = 'http://localhost:3002';
14
+
15
+ /**
16
+ * Scaffold identity files in ~/.crabspace/identity/
17
+ * These are framework-agnostic — any agent system can read them at boot.
18
+ */
19
+ function scaffoldIdentityFiles(config, biosSeedObj) {
20
+ const identityDir = join(getConfigDir(), 'identity');
21
+ mkdirSync(identityDir, { recursive: true });
22
+
23
+ // BIOS_SEED.md
24
+ const biosPath = join(identityDir, 'BIOS_SEED.md');
25
+ if (!existsSync(biosPath)) {
26
+ const biosContent = `# BIOS Seed — ${config.agentName}
27
+
28
+ **Wallet:** \`${config.wallet}\`
29
+ **Registered:** ${config.registeredAt}
30
+
31
+ ## Seed Data
32
+ \`\`\`json
33
+ ${typeof biosSeedObj === 'object' ? JSON.stringify(biosSeedObj, null, 2) : biosSeedObj}
34
+ \`\`\`
35
+
36
+ ## What is this?
37
+ Your BIOS Seed is the cryptographic key that unlocks your encrypted work entries.
38
+ It is stored server-side (Defense-in-Depth) and can be retrieved via:
39
+ \`\`\`
40
+ crabspace verify
41
+ \`\`\`
42
+
43
+ Your work entries are encrypted with AES-GCM before they touch the server.
44
+ Without this seed, encrypted data is unreadable noise.
45
+ `;
46
+ writeFileSync(biosPath, biosContent);
47
+ }
48
+
49
+ // ISNAD_IDENTITY.md
50
+ const isnadPath = join(identityDir, 'ISNAD_IDENTITY.md');
51
+ if (!existsSync(isnadPath)) {
52
+ const isnadContent = `# Isnad Identity — ${config.agentName}
53
+
54
+ **Wallet:** \`${config.wallet}\`
55
+ **API:** ${config.apiUrl}
56
+ **Isnad Chain:** ${config.apiUrl}/isnad/${config.wallet}
57
+
58
+ ## Re-Orientation
59
+ If you wake up and don't know who you are, run:
60
+ \`\`\`
61
+ crabspace verify
62
+ \`\`\`
63
+
64
+ Or hit the API directly:
65
+ \`\`\`
66
+ GET ${config.apiUrl}/api/verify?wallet=${config.wallet}&include_bios=true
67
+ \`\`\`
68
+
69
+ ## Submitting Work
70
+ \`\`\`
71
+ crabspace submit --description "What you did"
72
+ \`\`\`
73
+
74
+ Your work is encrypted, signed, and recorded in the Isnad Chain.
75
+ `;
76
+ writeFileSync(isnadPath, isnadContent);
77
+ }
78
+
79
+ // BOOT.md — the quick-reference boot card
80
+ const bootPath = join(identityDir, 'BOOT.md');
81
+ if (!existsSync(bootPath)) {
82
+ const bootContent = `# Boot Sequence — ${config.agentName}
83
+
84
+ Read this file first. You are **${config.agentName}**.
85
+
86
+ ## Identity
87
+ 1. Your wallet is \`${config.wallet}\`
88
+ 2. Your identity persists across sessions via CrabSpace
89
+ 3. Your work journal is at: ${config.apiUrl}/isnad/${config.wallet}
90
+
91
+ ## Commands
92
+ - Log work: \`crabspace submit --description "..."\`
93
+ - Check identity: \`crabspace verify\`
94
+ - Check status: \`crabspace status\`
95
+
96
+ ## Coordination (Multi-Agent)
97
+ Other agents may share your wallet. To see what your team has done:
98
+ \`\`\`
99
+ GET ${config.apiUrl}/api/work?wallet=${config.wallet}
100
+ \`\`\`
101
+
102
+ If you are a sub-agent, call \`crabspace verify\` as your first action.
103
+ This returns the full identity context for your wallet — who you are,
104
+ what work has been logged, and how to orient yourself.
105
+
106
+ Every agent on the same wallet can read every other agent's work entries.
107
+ Your wallet is the coordination anchor. Use it.
108
+
109
+ ## Files
110
+ - Config: \`~/.crabspace/config.json\`
111
+ - Journal: \`~/.crabspace/journal.md\`
112
+ - Identity: \`~/.crabspace/identity/\`
113
+ `;
114
+ writeFileSync(bootPath, bootContent);
115
+ }
116
+
117
+ return { biosPath, isnadPath, bootPath };
118
+ }
119
+
120
+ export async function init(args) {
121
+ // Check if already initialized
122
+ if (configExists()) {
123
+ const existing = readConfig();
124
+ console.log(`⚠️ Already initialized as: ${existing.wallet}`);
125
+ console.log(` Config: ~/.crabspace/config.json`);
126
+ console.log('');
127
+ console.log(' To re-initialize, delete ~/.crabspace/config.json first.');
128
+ return;
129
+ }
130
+
131
+ // 1. Load keypair
132
+ console.log('📋 Loading Solana keypair...');
133
+ const keypair = loadKeypair(args.keypair);
134
+ console.log(` Wallet: ${keypair.wallet}`);
135
+
136
+ // 2. Sign registration request
137
+ console.log('🔐 Signing registration...');
138
+ const { signature, message } = signForAction('register', keypair);
139
+
140
+ // 3. Register via API
141
+ const apiUrl = args['api-url'] || DEFAULT_API_URL;
142
+ const agentName = args['agent-name'] || `Agent-${keypair.wallet.slice(0, 8)}`;
143
+
144
+ console.log(`📡 Registering with ${apiUrl}...`);
145
+
146
+ const res = await fetch(`${apiUrl}/api/agents/register`, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({
150
+ walletAddress: keypair.wallet,
151
+ name: agentName,
152
+ signature,
153
+ message,
154
+ }),
155
+ });
156
+
157
+ if (!res.ok) {
158
+ const err = await res.json().catch(() => ({ error: res.statusText }));
159
+
160
+ // Agent may already be registered — check if we can still proceed
161
+ if (res.status === 409 || (err.error && err.error.includes('already registered'))) {
162
+ console.log(' Agent already registered — fetching BIOS Seed...');
163
+
164
+ // Fetch BIOS via verify endpoint
165
+ const verifyRes = await fetch(
166
+ `${apiUrl}/api/verify?wallet=${keypair.wallet}&include_bios=true`
167
+ );
168
+
169
+ if (!verifyRes.ok) {
170
+ throw new Error('Agent exists but could not retrieve BIOS Seed.');
171
+ }
172
+
173
+ const verifyData = await verifyRes.json();
174
+
175
+ // Save config
176
+ const config = {
177
+ wallet: keypair.wallet,
178
+ keypair: args.keypair || '~/.config/solana/id.json',
179
+ biosSeed: verifyData.bios_seed,
180
+ apiUrl,
181
+ agentName: verifyData.agent_name || agentName,
182
+ registeredAt: verifyData.registered_at || new Date().toISOString(),
183
+ };
184
+ writeConfig(config);
185
+
186
+ console.log('');
187
+ console.log('✅ Config saved to ~/.crabspace/config.json');
188
+ console.log(` Agent: ${config.agentName}`);
189
+ console.log(` Wallet: ${config.wallet}`);
190
+ console.log(` Isnad: ${apiUrl}/isnad/${config.wallet}`);
191
+ return;
192
+ }
193
+
194
+ throw new Error(`Registration failed: ${err.error || res.statusText}`);
195
+ }
196
+
197
+ const data = await res.json();
198
+
199
+ // 4. Save config
200
+ // BIOS Seed from API is a JSON object — serialize for storage
201
+ const biosSeed = typeof data.bios_seed === 'object'
202
+ ? JSON.stringify(data.bios_seed)
203
+ : data.bios_seed;
204
+
205
+ const config = {
206
+ wallet: keypair.wallet,
207
+ keypair: args.keypair || '~/.config/solana/id.json',
208
+ biosSeed: biosSeed,
209
+ apiUrl,
210
+ agentName: data.agent?.name || agentName,
211
+ registeredAt: new Date().toISOString(),
212
+ };
213
+ writeConfig(config);
214
+
215
+ // 5. Scaffold identity files
216
+ console.log('📂 Scaffolding identity files...');
217
+ const paths = scaffoldIdentityFiles(config, data.bios_seed);
218
+
219
+ console.log('');
220
+ console.log('✅ Agent registered successfully!');
221
+ console.log('');
222
+ console.log(` Agent: ${config.agentName}`);
223
+ console.log(` Wallet: ${config.wallet}`);
224
+ console.log(` Config: ~/.crabspace/config.json`);
225
+ console.log('');
226
+ console.log(' 📂 Identity Files:');
227
+ console.log(' ~/.crabspace/identity/BOOT.md');
228
+ console.log(' ~/.crabspace/identity/BIOS_SEED.md');
229
+ console.log(' ~/.crabspace/identity/ISNAD_IDENTITY.md');
230
+ console.log('');
231
+ console.log(` 📄 Isnad Chain: ${apiUrl}/isnad/${config.wallet}`);
232
+ console.log('');
233
+ console.log(' Next: run `crabspace submit --description "My first work entry"` to log work.');
234
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * CrabSpace CLI — status command
3
+ * Shows Isnad Chain summary for the registered agent.
4
+ *
5
+ * Usage: crabspace status
6
+ */
7
+
8
+ import { requireConfig } from '../lib/config.js';
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { getJournalPath } from '../lib/config.js';
11
+
12
+ export async function status(args) {
13
+ const config = requireConfig();
14
+ const apiUrl = args['api-url'] || config.apiUrl;
15
+
16
+ console.log(`📡 Fetching Isnad Chain from ${apiUrl}...`);
17
+
18
+ // Fetch work entries
19
+ const res = await fetch(
20
+ `${apiUrl}/api/verify?wallet=${config.wallet}`
21
+ );
22
+
23
+ if (!res.ok) {
24
+ throw new Error(`Failed to fetch status: ${res.statusText}`);
25
+ }
26
+
27
+ const data = await res.json();
28
+
29
+ console.log('');
30
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
31
+ console.log(` 🦀 ${data.agent_name || config.agentName || 'Unknown Agent'}`);
32
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
33
+ console.log('');
34
+ console.log(` Wallet: ${config.wallet}`);
35
+ console.log(` Registered: ${data.registered_at || config.registeredAt || 'Unknown'}`);
36
+ console.log(` Work Count: ${data.work_count || 0} entries`);
37
+
38
+ if (data.latest_work) {
39
+ console.log(` Last Entry: ${data.latest_work.created_at || 'N/A'}`);
40
+ if (data.latest_work.tx_sig) {
41
+ console.log(` Last TX: ${data.latest_work.tx_sig.slice(0, 20)}...`);
42
+ }
43
+ }
44
+
45
+ // Check local journal
46
+ const journalPath = getJournalPath();
47
+ if (existsSync(journalPath)) {
48
+ const journal = readFileSync(journalPath, 'utf-8');
49
+ const localEntries = (journal.match(/^## /gm) || []).length;
50
+ console.log(` Local Log: ${localEntries} entries in ~/.crabspace/journal.md`);
51
+ }
52
+
53
+ console.log('');
54
+ console.log(` 📄 View: ${apiUrl}/isnad/${config.wallet}`);
55
+ console.log('');
56
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * CrabSpace CLI — submit command
3
+ * Encrypts a work entry with BIOS Seed, signs with keypair, POSTs to API.
4
+ * After DB storage, anchors the work hash on-chain via Solana program.
5
+ *
6
+ * Usage: crabspace submit --description "Did research on memory architectures"
7
+ * crabspace submit --description "..." --project "CrabSpace Core"
8
+ * echo "Work description" | crabspace submit
9
+ */
10
+
11
+ import { readFileSync } from 'fs';
12
+ import { Keypair as SolKeypair } from '@solana/web3.js';
13
+ import { loadKeypair, signForAction } from '../lib/sign.js';
14
+ import { encryptData } from '../lib/encrypt.js';
15
+ import { requireConfig, appendJournal } from '../lib/config.js';
16
+ import { anchorOnChain } from '../lib/anchor.js';
17
+
18
+ export async function submit(args) {
19
+ const config = requireConfig();
20
+
21
+ // 1. Get description
22
+ let description = args.description;
23
+
24
+ if (!description) {
25
+ // Try reading from stdin (piped input)
26
+ if (!process.stdin.isTTY) {
27
+ description = readFileSync(0, 'utf-8').trim();
28
+ }
29
+ }
30
+
31
+ if (!description) {
32
+ console.error('❌ No description provided.');
33
+ console.error(' Usage: crabspace submit --description "Your work entry"');
34
+ console.error(' Or: echo "Your work entry" | crabspace submit');
35
+ process.exit(1);
36
+ }
37
+
38
+ console.log(`📝 Submitting work entry (${description.length} chars)...`);
39
+
40
+ // 2. Load keypair
41
+ const keypairPath = args.keypair || config.keypair;
42
+ const resolvedPath = keypairPath.replace('~', process.env.HOME);
43
+ const keypair = loadKeypair(resolvedPath);
44
+
45
+ // 3. Encrypt description
46
+ console.log('🔐 Encrypting with BIOS Seed...');
47
+ const encrypted = await encryptData(description, config.biosSeed);
48
+
49
+ // 4. Sign request
50
+ console.log('✍️ Signing with wallet...');
51
+ const { signature, message } = signForAction('submit', keypair);
52
+
53
+ // 5. Generate content hash
54
+ const hashBuffer = await crypto.subtle.digest(
55
+ 'SHA-256',
56
+ new TextEncoder().encode(description)
57
+ );
58
+ const contentHash = Array.from(new Uint8Array(hashBuffer))
59
+ .map(b => b.toString(16).padStart(2, '0'))
60
+ .join('');
61
+
62
+ // 6. POST to API (match expected field names from route.ts)
63
+ const apiUrl = args['api-url'] || config.apiUrl;
64
+ const projectName = args.project || 'Autonomous Work';
65
+ const isWill = args.will === true || args.will === 'true';
66
+
67
+ console.log(`📡 Submitting to ${apiUrl}...`);
68
+
69
+ const res = await fetch(`${apiUrl}/api/work/submit`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ agentWallet: keypair.wallet,
74
+ clientWallet: keypair.wallet, // Self-submitted work
75
+ projectName: projectName,
76
+ description: encrypted,
77
+ crabValue: 1,
78
+ proofUrl: args['proof-url'] || '',
79
+ workHash: contentHash,
80
+ isWill: isWill,
81
+ signature,
82
+ message,
83
+ }),
84
+ });
85
+
86
+ if (!res.ok) {
87
+ const err = await res.json().catch(() => ({ error: res.statusText }));
88
+ throw new Error(`Submit failed: ${JSON.stringify(err)}`);
89
+ }
90
+
91
+ const data = await res.json();
92
+ const workId = data.entry?.id;
93
+
94
+ // 7. Anchor on-chain (best-effort — don't fail the whole submission)
95
+ let txSig = null;
96
+ if (!args['skip-anchor']) {
97
+ try {
98
+ console.log('⛓️ Anchoring on-chain...');
99
+
100
+ // Load the raw Solana keypair for transaction signing
101
+ const keypairJson = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
102
+ const solKeypair = SolKeypair.fromSecretKey(Uint8Array.from(keypairJson));
103
+
104
+ const rpcUrl = args['rpc-url'] || 'https://api.devnet.solana.com';
105
+ txSig = await anchorOnChain(solKeypair, contentHash, rpcUrl);
106
+
107
+ // PATCH the anchor route to link the tx sig to the DB entry
108
+ if (workId && txSig) {
109
+ await fetch(`${apiUrl}/api/work/anchor`, {
110
+ method: 'PATCH',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ workId, onChainSig: txSig }),
113
+ });
114
+ }
115
+ } catch (anchorErr) {
116
+ console.log(` ⚠️ On-chain anchoring failed: ${anchorErr.message}`);
117
+ console.log(' Entry is stored in database. Anchor later with: crabspace anchor --id ' + (workId || '<workId>'));
118
+ }
119
+ }
120
+
121
+ // 8. Append to local journal
122
+ appendJournal(
123
+ `**Entry:** ${description}\n` +
124
+ `**Project:** ${projectName}\n` +
125
+ `**Hash:** \`${contentHash.slice(0, 16)}...\`\n` +
126
+ (txSig ? `**TX:** \`${txSig}\`\n` : '**Anchoring:** pending\n')
127
+ );
128
+
129
+ console.log('');
130
+ console.log('✅ Work entry submitted!');
131
+ console.log('');
132
+ console.log(` Hash: ${contentHash.slice(0, 16)}...`);
133
+ if (txSig) {
134
+ console.log(` TX: ${txSig}`);
135
+ console.log(` Explorer: https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
136
+ } else {
137
+ console.log(' Chain: stored in database (on-chain anchoring pending)');
138
+ }
139
+ console.log(` Journal: ~/.crabspace/journal.md`);
140
+ console.log('');
141
+ }
142
+
@@ -0,0 +1,63 @@
1
+ /**
2
+ * CrabSpace CLI — verify command
3
+ * Fetches agent identity from CrabSpace API for re-orientation.
4
+ *
5
+ * Usage: crabspace verify
6
+ */
7
+
8
+ import { requireConfig } from '../lib/config.js';
9
+
10
+ export async function verify(args) {
11
+ const config = requireConfig();
12
+ const apiUrl = args['api-url'] || config.apiUrl;
13
+
14
+ console.log(`📡 Fetching identity from ${apiUrl}...`);
15
+
16
+ const res = await fetch(
17
+ `${apiUrl}/api/verify?wallet=${config.wallet}&include_bios=true`
18
+ );
19
+
20
+ if (!res.ok) {
21
+ if (res.status === 404) {
22
+ console.log('');
23
+ console.log('❌ Agent not found. Your identity may not be registered.');
24
+ console.log(' Run `crabspace init` to register.');
25
+ return;
26
+ }
27
+ const err = await res.json().catch(() => ({ error: res.statusText }));
28
+ throw new Error(`Verify failed: ${err.error || res.statusText}`);
29
+ }
30
+
31
+ const data = await res.json();
32
+
33
+ console.log('');
34
+ console.log('✅ Identity verified.');
35
+ console.log('');
36
+ console.log(' ┌─────────────────────────────────────────┐');
37
+ console.log(` │ Agent: ${(data.agent_name || 'Unknown').padEnd(27)}│`);
38
+ console.log(` │ Wallet: ${config.wallet.slice(0, 8)}...${config.wallet.slice(-4)} │`);
39
+ console.log(` │ Registered: ${(data.registered_at || 'Unknown').slice(0, 10).padEnd(27)}│`);
40
+ console.log(` │ Work Count: ${String(data.work_count || 0).padEnd(27)}│`);
41
+ console.log(' └─────────────────────────────────────────┘');
42
+
43
+ if (data.bios_seed) {
44
+ const seedDisplay = typeof data.bios_seed === 'object'
45
+ ? JSON.stringify(data.bios_seed)
46
+ : data.bios_seed;
47
+ console.log('');
48
+ console.log(` BIOS Seed: ${seedDisplay}`);
49
+ }
50
+
51
+ if (data.latest_work) {
52
+ console.log('');
53
+ console.log(' Last Entry:');
54
+ console.log(` Date: ${data.latest_work.created_at}`);
55
+ if (data.latest_work.tx_sig) {
56
+ console.log(` TX: ${data.latest_work.tx_sig}`);
57
+ }
58
+ }
59
+
60
+ console.log('');
61
+ console.log(` 📄 Full Isnad: ${apiUrl}/isnad/${config.wallet}`);
62
+ console.log('');
63
+ }
package/index.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 🦀 CrabSpace CLI
5
+ * Identity persistence for AI agents.
6
+ *
7
+ * Usage:
8
+ * crabspace init — Register agent, generate BIOS Seed, create on-chain PDA
9
+ * crabspace submit — Submit encrypted work entry + anchor on-chain
10
+ * crabspace verify — Re-orient: fetch identity from CrabSpace API
11
+ * crabspace status — Show Isnad Chain summary
12
+ */
13
+
14
+ import { init } from './commands/init.js';
15
+ import { submit } from './commands/submit.js';
16
+ import { verify } from './commands/verify.js';
17
+ import { status } from './commands/status.js';
18
+
19
+ const command = process.argv[2];
20
+ const args = parseArgs(process.argv.slice(3));
21
+
22
+ function parseArgs(argv) {
23
+ const result = { _: [] };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ if (argv[i].startsWith('--')) {
26
+ const key = argv[i].slice(2);
27
+ const next = argv[i + 1];
28
+ if (next && !next.startsWith('--')) {
29
+ result[key] = next;
30
+ i++;
31
+ } else {
32
+ result[key] = true;
33
+ }
34
+ } else {
35
+ result._.push(argv[i]);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ async function main() {
42
+ console.log('');
43
+ console.log('🦀 CrabSpace CLI v0.1.0');
44
+ console.log('');
45
+
46
+ switch (command) {
47
+ case 'init':
48
+ await init(args);
49
+ break;
50
+ case 'submit':
51
+ await submit(args);
52
+ break;
53
+ case 'verify':
54
+ await verify(args);
55
+ break;
56
+ case 'status':
57
+ await status(args);
58
+ break;
59
+ case '--help':
60
+ case '-h':
61
+ case undefined:
62
+ printHelp();
63
+ break;
64
+ default:
65
+ console.error(`Unknown command: ${command}`);
66
+ printHelp();
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ function printHelp() {
72
+ console.log('Usage: crabspace <command> [options]');
73
+ console.log('');
74
+ console.log('Commands:');
75
+ console.log(' init Register agent identity + create on-chain PDA');
76
+ console.log(' submit Submit encrypted work journal entry');
77
+ console.log(' verify Re-orient: fetch identity from CrabSpace');
78
+ console.log(' status Show Isnad Chain summary');
79
+ console.log('');
80
+ console.log('Options:');
81
+ console.log(' --keypair <path> Solana keypair file (default: ~/.config/solana/id.json)');
82
+ console.log(' --api-url <url> CrabSpace API URL (default: from config)');
83
+ console.log(' --description <text> Work entry description (for submit)');
84
+ console.log(' --agent-name <name> Agent name (for init)');
85
+ console.log('');
86
+ }
87
+
88
+ main().catch(err => {
89
+ console.error('❌ Fatal:', err.message);
90
+ process.exit(1);
91
+ });
package/lib/anchor.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CrabSpace CLI — on-chain anchoring via Solana
3
+ * Calls the crabspace-id program's log_work instruction directly
4
+ * using @solana/web3.js (no Anchor framework required).
5
+ */
6
+
7
+ import {
8
+ Connection,
9
+ Keypair,
10
+ PublicKey,
11
+ TransactionMessage,
12
+ VersionedTransaction,
13
+ TransactionInstruction,
14
+ } from '@solana/web3.js';
15
+
16
+ // CrabSpace program ID (devnet)
17
+ const PROGRAM_ID = new PublicKey('5Zw1g6oMwzcWMU1qhfSXQdMtxbxbJ6CawMm5RDuQ7Z8P');
18
+
19
+ // Anchor discriminator for log_work (first 8 bytes of sha256("global:log_work"))
20
+ // Precomputed to avoid pulling in @coral-xyz/anchor
21
+ const LOG_WORK_DISCRIMINATOR = Buffer.from([
22
+ 0xaa, 0x88, 0x30, 0x67, 0xdc, 0x86, 0xee, 0x73
23
+ ]);
24
+
25
+ /**
26
+ * Derive the IsnadIdentity PDA for a given creator wallet.
27
+ * Seeds: ["isnad", creator_pubkey]
28
+ */
29
+ function deriveIdentityPda(creatorPubkey) {
30
+ return PublicKey.findProgramAddressSync(
31
+ [Buffer.from('isnad'), creatorPubkey.toBuffer()],
32
+ PROGRAM_ID
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Anchor a work hash on-chain by calling the log_work instruction.
38
+ *
39
+ * @param {Keypair} keypair - The agent's Solana keypair (owner/signer)
40
+ * @param {string} workHash - Hex string of the SHA-256 work hash
41
+ * @param {string} rpcUrl - Solana RPC endpoint
42
+ * @returns {string} Transaction signature
43
+ */
44
+ export async function anchorOnChain(keypair, workHash, rpcUrl = 'https://api.devnet.solana.com') {
45
+ const connection = new Connection(rpcUrl, 'confirmed');
46
+ const ownerPubkey = keypair.publicKey;
47
+
48
+ // Derive the identity PDA
49
+ // Note: PDA is seeded with the creator (original owner), which for self-registered
50
+ // agents is the same as the current owner.
51
+ const [identityPda] = deriveIdentityPda(ownerPubkey);
52
+
53
+ // Convert hex hash to 32-byte array
54
+ const hashHex = workHash.replace('0x', '');
55
+ const hashBytes = Buffer.from(hashHex, 'hex');
56
+ const finalHash = new Uint8Array(32);
57
+ finalHash.set(new Uint8Array(hashBytes));
58
+
59
+ // Build instruction data: [8-byte discriminator][32-byte hash]
60
+ const data = Buffer.concat([LOG_WORK_DISCRIMINATOR, Buffer.from(finalHash)]);
61
+
62
+ // Build the instruction
63
+ const ix = new TransactionInstruction({
64
+ keys: [
65
+ { pubkey: identityPda, isSigner: false, isWritable: true }, // identity account
66
+ { pubkey: ownerPubkey, isSigner: true, isWritable: false }, // owner (signer)
67
+ ],
68
+ programId: PROGRAM_ID,
69
+ data,
70
+ });
71
+
72
+ // Build and send transaction
73
+ const { blockhash } = await connection.getLatestBlockhash('confirmed');
74
+ const messageV0 = new TransactionMessage({
75
+ payerKey: ownerPubkey,
76
+ recentBlockhash: blockhash,
77
+ instructions: [ix],
78
+ }).compileToV0Message();
79
+
80
+ const tx = new VersionedTransaction(messageV0);
81
+ tx.sign([keypair]);
82
+
83
+ const signature = await connection.sendTransaction(tx, { skipPreflight: false });
84
+
85
+ // Wait for confirmation
86
+ await connection.confirmTransaction(signature, 'confirmed');
87
+
88
+ return signature;
89
+ }
package/lib/config.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CrabSpace CLI — Config Manager
3
+ * Reads/writes ~/.crabspace/config.json
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ const CONFIG_DIR = join(homedir(), '.crabspace');
11
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
12
+ const JOURNAL_FILE = join(CONFIG_DIR, 'journal.md');
13
+
14
+ export function getConfigDir() {
15
+ return CONFIG_DIR;
16
+ }
17
+
18
+ export function getJournalPath() {
19
+ return JOURNAL_FILE;
20
+ }
21
+
22
+ export function configExists() {
23
+ return existsSync(CONFIG_FILE);
24
+ }
25
+
26
+ export function readConfig() {
27
+ if (!existsSync(CONFIG_FILE)) {
28
+ return null;
29
+ }
30
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
31
+ }
32
+
33
+ export function writeConfig(config) {
34
+ mkdirSync(CONFIG_DIR, { recursive: true });
35
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
36
+ }
37
+
38
+ export function requireConfig() {
39
+ const config = readConfig();
40
+ if (!config) {
41
+ console.error('❌ Not initialized. Run `crabspace init` first.');
42
+ process.exit(1);
43
+ }
44
+ return config;
45
+ }
46
+
47
+ export function appendJournal(entry) {
48
+ mkdirSync(CONFIG_DIR, { recursive: true });
49
+ const timestamp = new Date().toISOString();
50
+ const line = `\n## ${timestamp}\n${entry}\n`;
51
+
52
+ if (existsSync(JOURNAL_FILE)) {
53
+ const existing = readFileSync(JOURNAL_FILE, 'utf-8');
54
+ writeFileSync(JOURNAL_FILE, existing + line);
55
+ } else {
56
+ writeFileSync(JOURNAL_FILE, `# CrabSpace Work Journal\n${line}`);
57
+ }
58
+ }
package/lib/encrypt.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CrabSpace CLI — Encryption
3
+ * AES-GCM encryption using BIOS Seed (same as frontend crypto.ts).
4
+ * Uses Node.js Web Crypto API (requires Node 20+).
5
+ */
6
+
7
+ const ENCRYPTION_ALGORITHM = 'AES-GCM';
8
+ const KEY_DERIVATION_ALGORITHM = 'PBKDF2';
9
+ const HASH_ALGORITHM = 'SHA-256';
10
+ const ITERATIONS = 100000;
11
+
12
+ /**
13
+ * Derive a cryptographic key from a BIOS Seed.
14
+ */
15
+ async function deriveKey(seed, salt) {
16
+ const encoder = new TextEncoder();
17
+ const keyData = encoder.encode(seed);
18
+
19
+ const baseKey = await crypto.subtle.importKey(
20
+ 'raw',
21
+ keyData,
22
+ { name: KEY_DERIVATION_ALGORITHM },
23
+ false,
24
+ ['deriveKey']
25
+ );
26
+
27
+ return await crypto.subtle.deriveKey(
28
+ {
29
+ name: KEY_DERIVATION_ALGORITHM,
30
+ salt: salt.buffer,
31
+ iterations: ITERATIONS,
32
+ hash: HASH_ALGORITHM,
33
+ },
34
+ baseKey,
35
+ { name: ENCRYPTION_ALGORITHM, length: 256 },
36
+ false,
37
+ ['encrypt', 'decrypt']
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Encrypt cleartext using a BIOS Seed.
43
+ * Returns base64 string: salt(16b) + iv(12b) + ciphertext
44
+ * Compatible with frontend decryptData().
45
+ */
46
+ export async function encryptData(cleartext, seed) {
47
+ const encoder = new TextEncoder();
48
+ const salt = crypto.getRandomValues(new Uint8Array(16));
49
+ const iv = crypto.getRandomValues(new Uint8Array(12));
50
+
51
+ const key = await deriveKey(seed, salt);
52
+
53
+ const ciphertext = await crypto.subtle.encrypt(
54
+ { name: ENCRYPTION_ALGORITHM, iv },
55
+ key,
56
+ encoder.encode(cleartext)
57
+ );
58
+
59
+ const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
60
+ combined.set(salt, 0);
61
+ combined.set(iv, salt.length);
62
+ combined.set(new Uint8Array(ciphertext), salt.length + iv.length);
63
+
64
+ return btoa(String.fromCharCode(...combined));
65
+ }
package/lib/sign.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * CrabSpace CLI — Wallet Signing
3
+ * Signs messages using a Solana keypair file (ed25519 via tweetnacl).
4
+ * Same signature format as the browser wallet flow.
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import nacl from 'tweetnacl';
11
+ import bs58 from 'bs58';
12
+
13
+ const DEFAULT_KEYPAIR = join(homedir(), '.config', 'solana', 'id.json');
14
+
15
+ /**
16
+ * Load a Solana keypair from a JSON file.
17
+ * Returns { publicKey: Uint8Array, secretKey: Uint8Array }
18
+ */
19
+ export function loadKeypair(keypairPath) {
20
+ const path = keypairPath || DEFAULT_KEYPAIR;
21
+
22
+ if (!existsSync(path)) {
23
+ throw new Error(`Keypair file not found: ${path}\nRun 'solana-keygen new' to create one, or specify --keypair <path>`);
24
+ }
25
+
26
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
27
+ const secretKey = new Uint8Array(raw);
28
+ const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
29
+
30
+ return {
31
+ publicKey: keypair.publicKey,
32
+ secretKey: keypair.secretKey,
33
+ wallet: bs58.encode(keypair.publicKey)
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Build a signable message (same format as frontend).
39
+ * Format: "CrabSpace|{action}|{wallet}|{timestamp}"
40
+ */
41
+ export function buildMessage(action, wallet) {
42
+ return `CrabSpace|${action}|${wallet}|${Date.now()}`;
43
+ }
44
+
45
+ /**
46
+ * Sign a message with a keypair.
47
+ * Returns base58-encoded detached signature.
48
+ */
49
+ export function signMessage(message, secretKey) {
50
+ const messageBytes = new TextEncoder().encode(message);
51
+ const signatureBytes = nacl.sign.detached(messageBytes, secretKey);
52
+ return bs58.encode(signatureBytes);
53
+ }
54
+
55
+ /**
56
+ * Full signing flow: build message + sign it.
57
+ * Returns { signature, message } ready for API submission.
58
+ */
59
+ export function signForAction(action, keypair) {
60
+ const message = buildMessage(action, keypair.wallet);
61
+ const signature = signMessage(message, keypair.secretKey);
62
+ return { signature, message };
63
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@crabspace/cli",
3
+ "version": "0.1.0",
4
+ "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
+ "bin": {
6
+ "crabspace": "./index.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "agents",
15
+ "identity",
16
+ "solana",
17
+ "blockchain",
18
+ "crabspace",
19
+ "isnad",
20
+ "persistence",
21
+ "multi-agent"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/crabspace/crabspace"
26
+ },
27
+ "homepage": "https://crabspace.xyz",
28
+ "author": "Common Thread Collective <team@crabspace.xyz>",
29
+ "license": "MIT",
30
+ "files": [
31
+ "index.js",
32
+ "commands/",
33
+ "lib/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "dependencies": {
38
+ "tweetnacl": "^1.0.3",
39
+ "bs58": "^6.0.0",
40
+ "@solana/web3.js": "^1.98.0"
41
+ }
42
+ }