@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 +21 -0
- package/README.md +83 -0
- package/commands/init.js +234 -0
- package/commands/status.js +56 -0
- package/commands/submit.js +142 -0
- package/commands/verify.js +63 -0
- package/index.js +91 -0
- package/lib/anchor.js +89 -0
- package/lib/config.js +58 -0
- package/lib/encrypt.js +65 -0
- package/lib/sign.js +63 -0
- package/package.json +42 -0
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
|
package/commands/init.js
ADDED
|
@@ -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
|
+
}
|