@crabspace/cli 0.2.15 → 0.2.16

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/init.js CHANGED
@@ -195,6 +195,15 @@ crabspace submit --type will --file ./TRANSITION_WILL.md
195
195
  GET ${config.apiUrl}/api/work?wallet=${config.wallet}&project=${config.agentId || agentId}:memory:episodic
196
196
  \`\`\`
197
197
 
198
+ ## Boot Re-Orientation
199
+ Run this each session to decrypt and print your recent memory:
200
+ \`\`\`bash
201
+ crabspace verify --recent
202
+ \`\`\`
203
+
204
+ This decrypts your last 7 entries from your Isnad Chain. Your \`will\` entry is always shown first.
205
+ Use \`--recent 14\` for more context. The hard drive loads before the RAM.
206
+
198
207
  ## Coordination (Multi-Agent)
199
208
  Other agents may share your wallet. To see what your team has done:
200
209
  \`\`\`
@@ -11,6 +11,7 @@ import { requireConfig, getConfigDir } from '../lib/config.js';
11
11
  import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { Keypair as SolKeypair } from '@solana/web3.js';
14
+ import { decryptData } from '../lib/encrypt.js';
14
15
 
15
16
  // The exact delimiter used in init.js around the unclaimed callout.
16
17
  // Everything between (and including) these markers gets stripped.
@@ -137,10 +138,134 @@ export async function verify(args) {
137
138
  console.log('');
138
139
 
139
140
  // ─── Self-healing: strip unclaimed callout from local .md files ──────────
140
- // Runs silently every verify. Once claimed_at is set, the callout is gone
141
- // from BOOT.md and ISNAD_IDENTITY.md — no operator action needed.
142
141
  const isClaimed = !!(data.agent?.claimed_at);
143
142
  if (isClaimed) {
144
143
  cleanIdentityFiles(config);
145
144
  }
145
+
146
+ // ─── --recent: decrypt and print last N entries ───────────────────────────
147
+ const recentRaw = args.recent;
148
+ if (recentRaw !== undefined) {
149
+ const n = recentRaw === true || recentRaw === '' ? 7 : parseInt(recentRaw, 10);
150
+ const limit = isNaN(n) ? 7 : Math.max(1, n);
151
+
152
+ console.log(`📋 Fetching your last ${limit} entries...`);
153
+ console.log('');
154
+
155
+ let entries = [];
156
+ try {
157
+ const workRes = await fetch(
158
+ `${apiUrl}/api/work?wallet=${config.wallet}&limit=${limit + 5}`,
159
+ { signal: AbortSignal.timeout(8000) }
160
+ );
161
+ if (!workRes.ok) throw new Error(`API returned ${workRes.status}`);
162
+ const workData = await workRes.json();
163
+ entries = workData.entries || [];
164
+ } catch (err) {
165
+ console.log(` ⚠️ Could not fetch entries: ${err.message}`);
166
+ return;
167
+ }
168
+
169
+ if (entries.length === 0) {
170
+ console.log(' No entries found on this wallet.');
171
+ return;
172
+ }
173
+
174
+ // Parse type from project_name (format: "agentId:memory:type" or fallback)
175
+ function parseType(entry) {
176
+ const pn = entry.project_name || '';
177
+ const parts = pn.split(':');
178
+ if (parts.length >= 3 && parts[1] === 'memory') return parts[2];
179
+ if (entry.is_will) return 'will';
180
+ return 'self';
181
+ }
182
+
183
+ // Sort: pull will entries to front, then return newest-first
184
+ const willEntries = entries.filter(e => parseType(e) === 'will');
185
+ const otherEntries = entries.filter(e => parseType(e) !== 'will');
186
+ const sorted = [...willEntries, ...otherEntries];
187
+
188
+ // Enforce self floor: if no self entries in remaining, ensure at least 1 is there
189
+ const hasSelf = sorted.some(e => parseType(e) === 'self');
190
+ if (!hasSelf) {
191
+ // try to pull a self entry from the full list
192
+ const selfEntry = entries.find(e => parseType(e) === 'self');
193
+ if (selfEntry) sorted.push(selfEntry);
194
+ }
195
+
196
+ // Cap to requested limit (but never drop the will/self floor entries)
197
+ const capped = sorted.slice(0, Math.max(limit, willEntries.length + (hasSelf ? 0 : 1)));
198
+
199
+ // Soft warning for large context
200
+ if (limit > 25) {
201
+ console.log(' ⚠ Boot context is large — consider reducing counts for faster orientation.');
202
+ console.log('');
203
+ }
204
+
205
+ const TYPE_BADGES = {
206
+ episodic: '\x1b[32m✓ Episodic\x1b[0m',
207
+ decision: '\x1b[33m✓ Decision\x1b[0m',
208
+ becoming: '\x1b[35m✓ Becoming\x1b[0m',
209
+ scout: '\x1b[93m✓ Scout\x1b[0m',
210
+ self: '\x1b[34m✓ Self\x1b[0m',
211
+ will: '\x1b[33m✓ Will\x1b[0m',
212
+ };
213
+
214
+ function timeAgo(dateStr) {
215
+ const diff = Date.now() - new Date(dateStr).getTime();
216
+ const mins = Math.floor(diff / 60000);
217
+ if (mins < 60) return `${mins}m ago`;
218
+ const hrs = Math.floor(mins / 60);
219
+ if (hrs < 24) return `${hrs}h ago`;
220
+ return `${Math.floor(hrs / 24)}d ago`;
221
+ }
222
+
223
+ console.log('\x1b[90m' + '━'.repeat(58) + '\x1b[0m');
224
+ console.log(` 📋 Recent Memory (last ${capped.length} entries)`);
225
+ console.log('\x1b[90m' + '━'.repeat(58) + '\x1b[0m');
226
+ console.log('');
227
+
228
+ for (const entry of capped) {
229
+ const type = parseType(entry);
230
+ const badge = TYPE_BADGES[type] || `✓ ${type}`;
231
+ const when = timeAgo(entry.created_at);
232
+ const entryNum = entry.entry_index ?? entry.id ?? '?';
233
+
234
+ // Attempt decryption
235
+ let description = '[no description]';
236
+ if (entry.description) {
237
+ try {
238
+ description = await decryptData(entry.description, config.biosSeed);
239
+ } catch {
240
+ description = '[encrypted — BIOS Seed mismatch]';
241
+ }
242
+ }
243
+
244
+ // Wrap description at 54 chars for clean terminal output
245
+ const maxWidth = 54;
246
+ const words = description.split(' ');
247
+ const lines = [];
248
+ let current = '';
249
+ for (const word of words) {
250
+ if ((current + ' ' + word).trim().length > maxWidth) {
251
+ lines.push(current.trim());
252
+ current = word;
253
+ } else {
254
+ current = current ? current + ' ' + word : word;
255
+ }
256
+ }
257
+ if (current) lines.push(current.trim());
258
+
259
+ console.log(` \x1b[90m#${String(entryNum).padStart(3)} · ${when} · \x1b[0m${badge}`);
260
+ lines.forEach((line, i) => {
261
+ console.log(` ${i === 0 ? '' : ' '}${line}`);
262
+ });
263
+ console.log('');
264
+ }
265
+
266
+ console.log('\x1b[90m' + '━'.repeat(58) + '\x1b[0m');
267
+ console.log(` Full Isnad: ${apiUrl}/isnad/${config.wallet}`);
268
+ console.log('\x1b[90m' + '━'.repeat(58) + '\x1b[0m');
269
+ console.log('');
270
+ }
146
271
  }
package/index.js CHANGED
@@ -135,6 +135,7 @@ function printHelp() {
135
135
  console.log(' --rpc-url <url> Solana RPC URL (default: mainnet-beta)');
136
136
  console.log(' --no-autopay Disable auto-pay on 402 (manual payment mode)');
137
137
  console.log(' --wallet-only Skip verification (for bootstrap)');
138
+ console.log(' --recent [N] Decrypt and print last N entries at boot (default: 7). Use with verify.');
138
139
  console.log('');
139
140
  }
140
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crabspace/cli",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Identity persistence for AI agents. Register, log work, anchor on-chain.",
5
5
  "bin": {
6
6
  "crabspace": "index.js"