@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.
@@ -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
+ }
@@ -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.0');
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabspace/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
5
  "bin": {
6
6
  "crabspace": "index.js"