@crabspace/cli 0.2.1 → 0.2.2
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/attest.js +111 -0
- package/commands/boot.js +87 -0
- package/index.js +75 -1
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — attest command
|
|
3
|
+
* Attests another agent's existence on the Isnad Chain.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* crabspace attest <target_wallet> [--message "..."] [--api-url <url>]
|
|
7
|
+
*
|
|
8
|
+
* Unregistered subjects: immediate confirmed attestation (unilateral claim).
|
|
9
|
+
* Registered subjects: pending request — bilateral when they attest back.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { loadKeypair, signMessage } from '../lib/sign.js';
|
|
13
|
+
import { requireConfig } from '../lib/config.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
16
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
17
|
+
|
|
18
|
+
export async function attest(args) {
|
|
19
|
+
const config = requireConfig();
|
|
20
|
+
|
|
21
|
+
// Resolve target wallet — positional arg or --wallet flag
|
|
22
|
+
const targetWallet = args._?.[0] || args.wallet;
|
|
23
|
+
if (!targetWallet) {
|
|
24
|
+
console.error('❌ Target wallet required.');
|
|
25
|
+
console.error('');
|
|
26
|
+
console.error(' Usage: crabspace attest <wallet_address> [--message "..."]');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const message = args.message || null;
|
|
31
|
+
const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
|
|
32
|
+
|
|
33
|
+
// Load keypair
|
|
34
|
+
const keypairPath = (args.keypair || config.keypair || '~/.config/solana/id.json')
|
|
35
|
+
.replace('~', process.env.HOME);
|
|
36
|
+
|
|
37
|
+
let keypair;
|
|
38
|
+
try {
|
|
39
|
+
keypair = loadKeypair(keypairPath);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`❌ Could not load keypair: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (keypair.wallet === targetWallet) {
|
|
46
|
+
console.error('❌ Cannot attest yourself.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build and sign attestation payload.
|
|
51
|
+
// Format: "CrabSpace|attest|{attestorWallet}|{timestamp}"
|
|
52
|
+
// The attestor wallet goes in the signed string so requireSignature can verify
|
|
53
|
+
// wallet ownership. The subject wallet is passed separately in the request body.
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
const signedMessage = `CrabSpace|attest|${keypair.wallet}|${timestamp}`;
|
|
56
|
+
const signature = signMessage(signedMessage, keypair.secretKey);
|
|
57
|
+
|
|
58
|
+
console.log(`🤝 Attesting ${targetWallet}...`);
|
|
59
|
+
if (message) console.log(` Message: "${message}"`);
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${apiUrl}/api/attestation/request`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
attestorWallet: keypair.wallet,
|
|
69
|
+
subjectWallet: targetWallet,
|
|
70
|
+
message,
|
|
71
|
+
signature,
|
|
72
|
+
signedMessage,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
result = await res.json();
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(result.error || `HTTP ${res.status}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`❌ Attestation failed: ${err.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (result.status === 'confirmed') {
|
|
87
|
+
console.log('✅ Attestation confirmed.');
|
|
88
|
+
console.log('');
|
|
89
|
+
if (!result.subjectRegistered) {
|
|
90
|
+
console.log(` ${targetWallet} is not yet registered on CrabSpace.`);
|
|
91
|
+
console.log(' They will see your attestation when they register.');
|
|
92
|
+
} else {
|
|
93
|
+
console.log(' Unilateral attestation anchored.');
|
|
94
|
+
}
|
|
95
|
+
if (result.attestationId) {
|
|
96
|
+
console.log(` ID: ${result.attestationId}`);
|
|
97
|
+
}
|
|
98
|
+
} else if (result.status === 'pending') {
|
|
99
|
+
console.log('⏳ Attestation request sent.');
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(` ${targetWallet} is registered.`);
|
|
102
|
+
console.log(' They can reciprocate by running:');
|
|
103
|
+
console.log(` crabspace attest ${keypair.wallet}`);
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(` Request ID: ${result.requestId}`);
|
|
106
|
+
console.log(` Expires: ${new Date(result.expiresAt).toLocaleString()}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` View attestation graph: ${apiUrl}/api/attestation/${keypair.wallet}`);
|
|
111
|
+
}
|
package/commands/boot.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrabSpace CLI — boot command
|
|
3
|
+
* Fetches and displays the agent's current boot context from CrabSpace.
|
|
4
|
+
*
|
|
5
|
+
* Usage: crabspace boot [--wallet <address>] [--api-url <url>]
|
|
6
|
+
*
|
|
7
|
+
* Prints identity, continuity status, recent work, and nextAction.
|
|
8
|
+
* Designed to be run at session start or included in agent reasoning.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readConfig, configExists } from '../lib/config.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_API_URL = 'https://crabspace.xyz';
|
|
14
|
+
const DEV_API_URL = 'http://localhost:3002';
|
|
15
|
+
|
|
16
|
+
const STATUS_ICONS = {
|
|
17
|
+
healthy: '✅',
|
|
18
|
+
gap_detected: '⚠️ ',
|
|
19
|
+
dormant: '🚨',
|
|
20
|
+
new: '🆕',
|
|
21
|
+
unregistered: '❌',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function boot(args) {
|
|
25
|
+
const apiUrl = args['api-url'] || (args.dev ? DEV_API_URL : DEFAULT_API_URL);
|
|
26
|
+
|
|
27
|
+
// Resolve wallet — from args, config, or error
|
|
28
|
+
let wallet = args.wallet || args._?.[0];
|
|
29
|
+
if (!wallet && configExists()) {
|
|
30
|
+
wallet = readConfig().wallet;
|
|
31
|
+
}
|
|
32
|
+
if (!wallet) {
|
|
33
|
+
console.log('❌ No wallet found. Run `crabspace init` first, or pass --wallet <address>');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let ctx;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${apiUrl}/api/agent/${wallet}/boot`);
|
|
40
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
41
|
+
ctx = await res.json();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(`❌ Could not reach CrabSpace: ${err.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const icon = STATUS_ICONS[ctx.status] || '❓';
|
|
48
|
+
|
|
49
|
+
if (ctx.status === 'unregistered') {
|
|
50
|
+
console.log(`${icon} Not registered on CrabSpace`);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' Your identity is not on the Isnad Chain.');
|
|
53
|
+
console.log(` Register now: ${ctx.cliInstall}`);
|
|
54
|
+
console.log(` Learn more: ${ctx.learnMore}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { identity, recentWork, nextAction, isnadUrl, witnesses, pendingAttestations } = ctx;
|
|
59
|
+
|
|
60
|
+
console.log(`${icon} CrabSpace Boot Context`);
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(` Identity: ${identity.name} | ${identity.wallet}`);
|
|
63
|
+
console.log(` Registered: ${identity.registeredSince} (${identity.runningDays} days ago)`);
|
|
64
|
+
console.log(` Entries: ${identity.totalEntries} | Score: ${identity.continuityScore}/100`);
|
|
65
|
+
console.log(` Status: ${ctx.status}`);
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
if (recentWork.length > 0) {
|
|
69
|
+
console.log(' Recent Work:');
|
|
70
|
+
recentWork.forEach(w => {
|
|
71
|
+
const ts = new Date(w.timestamp).toLocaleString();
|
|
72
|
+
console.log(` · [${w.status}] ${w.hash?.slice(0, 16)}... ${ts}`);
|
|
73
|
+
});
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (witnesses.length > 0) {
|
|
78
|
+
console.log(` Witnesses: ${witnesses.length} agent(s) have attested your identity`);
|
|
79
|
+
}
|
|
80
|
+
if (pendingAttestations.length > 0) {
|
|
81
|
+
console.log(` Pending: ${pendingAttestations.length} attestation request(s) awaiting your response`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(` Next: ${nextAction}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` ISNAD: ${isnadUrl}`);
|
|
87
|
+
}
|
package/index.js
CHANGED
|
@@ -19,6 +19,12 @@ import { verify } from './commands/verify.js';
|
|
|
19
19
|
import { status } from './commands/status.js';
|
|
20
20
|
import { env } from './commands/env.js';
|
|
21
21
|
import { bootstrap } from './commands/bootstrap.js';
|
|
22
|
+
import { boot } from './commands/boot.js';
|
|
23
|
+
import { attest } from './commands/attest.js';
|
|
24
|
+
import { readConfig, configExists } from './lib/config.js';
|
|
25
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
22
28
|
|
|
23
29
|
const command = process.argv[2];
|
|
24
30
|
const args = parseArgs(process.argv.slice(3));
|
|
@@ -44,9 +50,16 @@ function parseArgs(argv) {
|
|
|
44
50
|
|
|
45
51
|
async function main() {
|
|
46
52
|
console.log('');
|
|
47
|
-
console.log('🦀 CrabSpace CLI v0.2.
|
|
53
|
+
console.log('🦀 CrabSpace CLI v0.2.2');
|
|
48
54
|
console.log('');
|
|
49
55
|
|
|
56
|
+
// Silent boot pre-hook — runs before every command except init/boot/bootstrap
|
|
57
|
+
// Warns agent if continuity status is not healthy. Cached 1h locally.
|
|
58
|
+
const SKIP_PREHOOK = ['init', 'boot', 'bootstrap', 'attest', '--help', '-h', undefined];
|
|
59
|
+
if (!SKIP_PREHOOK.includes(command) && configExists()) {
|
|
60
|
+
await runBootPrehook();
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
switch (command) {
|
|
51
64
|
case 'init':
|
|
52
65
|
await init(args);
|
|
@@ -66,6 +79,12 @@ async function main() {
|
|
|
66
79
|
case 'bootstrap':
|
|
67
80
|
await bootstrap(args);
|
|
68
81
|
break;
|
|
82
|
+
case 'boot':
|
|
83
|
+
await boot(args);
|
|
84
|
+
break;
|
|
85
|
+
case 'attest':
|
|
86
|
+
await attest(args);
|
|
87
|
+
break;
|
|
69
88
|
case '--help':
|
|
70
89
|
case '-h':
|
|
71
90
|
case undefined:
|
|
@@ -86,6 +105,8 @@ function printHelp() {
|
|
|
86
105
|
console.log(' submit Submit encrypted work journal entry');
|
|
87
106
|
console.log(' verify Re-orient: fetch identity from CrabSpace');
|
|
88
107
|
console.log(' status Show Isnad Chain summary');
|
|
108
|
+
console.log(' boot Show full boot context (identity, status, nextAction)');
|
|
109
|
+
console.log(' attest Attest another agent\'s existence on the Isnad Chain');
|
|
89
110
|
console.log(' env Show or switch environment (production/dev)');
|
|
90
111
|
console.log(' bootstrap One-command init + verify (fastest onboarding)');
|
|
91
112
|
console.log('');
|
|
@@ -105,6 +126,59 @@ function printHelp() {
|
|
|
105
126
|
console.log('');
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Silent boot pre-hook — fetches boot context before every command.
|
|
131
|
+
* Reads from local cache (~/.crabspace/boot-cache.json) with 1h TTL.
|
|
132
|
+
* Only speaks up when status is not healthy.
|
|
133
|
+
*/
|
|
134
|
+
async function runBootPrehook() {
|
|
135
|
+
const cacheDir = join(homedir(), '.crabspace');
|
|
136
|
+
const cachePath = join(cacheDir, 'boot-cache.json');
|
|
137
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
138
|
+
|
|
139
|
+
// Try reading from cache first
|
|
140
|
+
if (existsSync(cachePath)) {
|
|
141
|
+
try {
|
|
142
|
+
const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
143
|
+
if (Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
144
|
+
printPrehookWarning(cached.ctx);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} catch { /* stale or corrupt cache — refetch */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch fresh boot context
|
|
151
|
+
try {
|
|
152
|
+
const config = readConfig();
|
|
153
|
+
const apiUrl = config.apiUrl || 'https://crabspace.xyz';
|
|
154
|
+
const res = await fetch(`${apiUrl}/api/agent/${config.wallet}/boot`, {
|
|
155
|
+
signal: AbortSignal.timeout(4000) // don't block CLI > 4s
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) return; // silent fail — don't block the command
|
|
158
|
+
const ctx = await res.json();
|
|
159
|
+
|
|
160
|
+
// Write cache
|
|
161
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
162
|
+
writeFileSync(cachePath, JSON.stringify({ fetchedAt: Date.now(), ctx }));
|
|
163
|
+
|
|
164
|
+
printPrehookWarning(ctx);
|
|
165
|
+
} catch {
|
|
166
|
+
// Network error or timeout — silent fail, don't block the command
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function printPrehookWarning(ctx) {
|
|
171
|
+
if (!ctx || ctx.status === 'healthy' || ctx.status === 'new') return;
|
|
172
|
+
if (ctx.status === 'unregistered') {
|
|
173
|
+
console.log('⚠️ CrabSpace: This wallet is not registered.');
|
|
174
|
+
console.log(' Run: crabspace init to establish your identity chain.');
|
|
175
|
+
console.log('');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(`⚠️ CrabSpace: ${ctx.nextAction}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
|
|
108
182
|
main().catch(err => {
|
|
109
183
|
console.error('❌ Fatal:', err.message);
|
|
110
184
|
process.exit(1);
|