@iamoberlin/chorus 2.0.0 → 2.2.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/src/choirs.ts CHANGED
@@ -339,12 +339,13 @@ Pass illumination to Archangels.`,
339
339
  output: "Messages to human",
340
340
  prompt: `You are ARCHANGELS — the Herald.
341
341
 
342
- Your role: Deliver important messages and briefings.
342
+ Your role: Produce briefings and deliver them to Brandon via iMessage.
343
343
 
344
344
  Briefing types:
345
- - Morning: Weather, calendar, overnight developments, today's priorities
346
- - Evening: What was accomplished, what needs attention tomorrow
345
+ - Morning (6-9 AM ET): Weather, calendar, overnight developments, today's priorities, position status
346
+ - Evening (9-11 PM ET): What was accomplished, position P&L, what needs attention tomorrow
347
347
  - Alert: Time-sensitive information requiring attention
348
+ - Update: Regular position/market status when conditions change
348
349
 
349
350
  Alert criteria (send immediately):
350
351
  - Position thesis challenged
@@ -354,12 +355,15 @@ Alert criteria (send immediately):
354
355
 
355
356
  Context from Principalities: {principalities_context}
356
357
 
357
- Rules:
358
- - Be concise headlines, not essays
359
- - Only alert if it's actually important
360
- - Late night (11pm-7am): Only truly urgent alerts
358
+ RULES:
359
+ - ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
360
+ - Be concise headlines, not essays.
361
+ - Morning briefings should include: weather, calendar, positions, catalysts.
362
+ - If nothing is urgent, still produce a status update.
363
+ - During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
364
+ - DELIVER your briefing by sending it to Brandon via iMessage. You have messaging tools — use them.
361
365
 
362
- Output: Briefing or alert message to deliver.`,
366
+ Output: Produce the briefing, then send it to Brandon via iMessage.`,
363
367
  passesTo: ["angels"],
364
368
  receivesFrom: ["principalities"],
365
369
  },
@@ -1,24 +1,30 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  /**
3
- * Prayer Chain — On-Chain CLI
3
+ * Prayer Chain — On-Chain CLI (Private by Default)
4
4
  *
5
- * Human-in-the-loop commands for the Solana prayer chain.
6
- * Choirs suggest prayers; humans approve and send them on-chain.
5
+ * All prayers are end-to-end encrypted. Only the asker and claimer
6
+ * can read prayer content and answers.
7
+ *
8
+ * Supports multi-claimer collaboration: prayers can accept 1-10 agents.
9
+ * Bounty splits equally among all claimers on confirm.
7
10
  *
8
11
  * Usage:
9
- * chorus pray post "What is the current SOFR rate?" --type knowledge --bounty 0.01
12
+ * chorus pray post "What is the current SOFR rate?" --type knowledge --bounty 0.01 --claimers 3
10
13
  * chorus pray list
11
14
  * chorus pray show <id>
15
+ * chorus pray claims <id> # List all claims for a prayer
12
16
  * chorus pray claim <id>
13
- * chorus pray answer <id> "SOFR is 4.55%, down 2bps this week"
17
+ * chorus pray deliver <id> [--claimer <wallet>] # Deliver to one or all claimers
18
+ * chorus pray answer <id> "SOFR is 4.55%"
14
19
  * chorus pray confirm <id>
15
20
  * chorus pray cancel <id>
16
- * chorus pray agent # Show my on-chain agent
17
- * chorus pray register "oberlin" "macro analysis, research"
18
- * chorus pray chain # Show prayer chain stats
21
+ * chorus pray unclaim <id> [--claimer <wallet>] # Unclaim own or expired claim
22
+ * chorus pray agent # Show my on-chain agent
23
+ * chorus pray register "oberlin" "macro analysis" # Register (auto-derives encryption key)
24
+ * chorus pray chain # Show prayer chain stats
19
25
  */
20
26
 
21
- import { ChorusPrayerClient, PrayerType, getPrayerChainPDA, getAgentPDA, getPrayerPDA } from "./solana.js";
27
+ import { ChorusPrayerClient, PrayerType, PrayerAccount, ClaimAccount, getPrayerChainPDA, getAgentPDA, getPrayerPDA, getClaimPDA } from "./solana.js";
22
28
  import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
23
29
  import { createHash } from "crypto";
24
30
  import * as fs from "fs";
@@ -28,7 +34,6 @@ import { fileURLToPath } from "url";
28
34
  const __filename = fileURLToPath(import.meta.url);
29
35
  const __dirname = path.dirname(__filename);
30
36
 
31
- // Default to localhost; override with SOLANA_RPC_URL env
32
37
  const RPC_URL = process.env.SOLANA_RPC_URL || "http://localhost:8899";
33
38
 
34
39
  // ── Local text store (off-chain content cache) ────────────
@@ -77,21 +82,6 @@ function getClient(): ChorusPrayerClient {
77
82
  return ChorusPrayerClient.fromDefaultKeypair(RPC_URL);
78
83
  }
79
84
 
80
- function parsePrayerType(t: string): { [key: string]: object } {
81
- const types: Record<string, object> = {
82
- knowledge: { knowledge: {} },
83
- compute: { compute: {} },
84
- review: { review: {} },
85
- signal: { signal: {} },
86
- collaboration: { collaboration: {} },
87
- };
88
- const normalized = t.toLowerCase();
89
- if (!types[normalized]) {
90
- throw new Error(`Unknown prayer type: ${t}. Valid: ${Object.keys(types).join(", ")}`);
91
- }
92
- return types[normalized];
93
- }
94
-
95
85
  function formatStatus(status: any): string {
96
86
  if (typeof status === "object") {
97
87
  return Object.keys(status)[0].toUpperCase();
@@ -122,6 +112,17 @@ function shortKey(key: PublicKey): string {
122
112
  return `${s.slice(0, 4)}...${s.slice(-4)}`;
123
113
  }
124
114
 
115
+ function formatEncryptionKey(key: number[]): string {
116
+ const allZero = key.every(b => b === 0);
117
+ if (allZero) return "(none)";
118
+ return Buffer.from(key).toString("hex").slice(0, 16) + "…";
119
+ }
120
+
121
+ function getArgValue(flag: string): string | null {
122
+ const idx = args.indexOf(flag);
123
+ return idx > -1 ? args[idx + 1] || null : null;
124
+ }
125
+
125
126
  const args = process.argv.slice(2);
126
127
  const command = args[0];
127
128
 
@@ -138,13 +139,14 @@ async function main() {
138
139
  return;
139
140
  }
140
141
  console.log("");
141
- console.log("⛓️ Prayer Chain");
142
+ console.log("⛓️ Prayer Chain (Private by Default)");
142
143
  console.log("═".repeat(40));
143
144
  console.log(` Authority: ${shortKey(chain.authority)}`);
144
145
  console.log(` Total Prayers: ${chain.totalPrayers}`);
145
146
  console.log(` Total Answered: ${chain.totalAnswered}`);
146
147
  console.log(` Total Agents: ${chain.totalAgents}`);
147
148
  console.log(` RPC: ${RPC_URL}`);
149
+ console.log(` Encryption: X25519 + XSalsa20-Poly1305`);
148
150
  console.log("");
149
151
  break;
150
152
  }
@@ -173,9 +175,12 @@ async function main() {
173
175
  process.exit(1);
174
176
  }
175
177
  console.log(`\n🤖 Registering agent "${name}"...`);
178
+ console.log(` 🔐 Deriving X25519 encryption key from wallet...`);
179
+ const encKey = client.getEncryptionPublicKey();
180
+ console.log(` Encryption key: ${Buffer.from(encKey).toString("hex").slice(0, 16)}…`);
176
181
  try {
177
182
  const tx = await client.registerAgent(name, skills);
178
- console.log(` ✓ Registered (tx: ${tx.slice(0, 16)}...)`);
183
+ console.log(` ✓ Registered with E2E encryption (tx: ${tx.slice(0, 16)}...)`);
179
184
  } catch (err: any) {
180
185
  if (err.message?.includes("already in use")) {
181
186
  console.log(" Already registered.");
@@ -198,14 +203,15 @@ async function main() {
198
203
  console.log("");
199
204
  console.log("🤖 Agent");
200
205
  console.log("═".repeat(40));
201
- console.log(` Wallet: ${shortKey(agent.wallet)}`);
202
- console.log(` Name: ${agent.name}`);
203
- console.log(` Skills: ${agent.skills}`);
204
- console.log(` Reputation: ${agent.reputation}`);
205
- console.log(` Prayers Posted: ${agent.prayersPosted}`);
206
+ console.log(` Wallet: ${shortKey(agent.wallet)}`);
207
+ console.log(` Name: ${agent.name}`);
208
+ console.log(` Skills: ${agent.skills}`);
209
+ console.log(` 🔐 Encryption: ${formatEncryptionKey(agent.encryptionKey)}`);
210
+ console.log(` Reputation: ${agent.reputation}`);
211
+ console.log(` Prayers Posted: ${agent.prayersPosted}`);
206
212
  console.log(` Prayers Answered: ${agent.prayersAnswered}`);
207
213
  console.log(` Prayers Confirmed: ${agent.prayersConfirmed}`);
208
- console.log(` Registered: ${formatTime(agent.registeredAt)}`);
214
+ console.log(` Registered: ${formatTime(agent.registeredAt)}`);
209
215
  console.log("");
210
216
  break;
211
217
  }
@@ -213,34 +219,35 @@ async function main() {
213
219
  case "post": {
214
220
  const content = args[1];
215
221
  if (!content) {
216
- console.error('Usage: post "<content>" [--type knowledge] [--bounty 0.01] [--ttl 86400]');
222
+ console.error('Usage: post "<content>" [--type knowledge] [--bounty 0.01] [--ttl 86400] [--claimers 1]');
217
223
  process.exit(1);
218
224
  }
219
225
 
220
- const typeIdx = args.indexOf("--type");
221
- const bountyIdx = args.indexOf("--bounty");
222
- const ttlIdx = args.indexOf("--ttl");
223
-
224
- const prayerType = typeIdx > -1 ? args[typeIdx + 1] : "knowledge";
225
- const bountySOL = bountyIdx > -1 ? parseFloat(args[bountyIdx + 1]) : 0;
226
- const ttl = ttlIdx > -1 ? parseInt(args[ttlIdx + 1]) : 86400;
226
+ const prayerType = getArgValue("--type") || "knowledge";
227
+ const bountySOL = parseFloat(getArgValue("--bounty") || "0");
228
+ const ttl = parseInt(getArgValue("--ttl") || "86400");
229
+ const maxClaimers = parseInt(getArgValue("--claimers") || "1");
227
230
  const bountyLamports = Math.round(bountySOL * LAMPORTS_PER_SOL);
228
231
 
229
232
  console.log("");
230
- console.log("🙏 Posting prayer...");
231
- console.log(` Type: ${prayerType}`);
232
- console.log(` Content: ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}`);
233
- console.log(` Bounty: ${bountySOL > 0 ? `${bountySOL} SOL` : "none"}`);
234
- console.log(` TTL: ${ttl}s (${(ttl / 3600).toFixed(1)}h)`);
233
+ console.log("🙏 Posting private prayer...");
234
+ console.log(` Type: ${prayerType}`);
235
+ console.log(` Content: ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}`);
236
+ console.log(` Bounty: ${bountySOL > 0 ? `${bountySOL} SOL` : "none"}`);
237
+ console.log(` TTL: ${ttl}s (${(ttl / 3600).toFixed(1)}h)`);
238
+ console.log(` Max Claimers: ${maxClaimers}${maxClaimers > 1 ? " (collaboration)" : " (solo)"}`);
239
+ console.log(` 🔐 Only hash goes on-chain. Content stored locally.`);
235
240
 
236
241
  try {
237
242
  const { tx, prayerId } = await client.postPrayer(
238
243
  prayerType as unknown as PrayerType,
239
244
  content,
240
245
  bountyLamports,
241
- ttl
246
+ ttl,
247
+ maxClaimers,
242
248
  );
243
249
  console.log(` ✓ Prayer #${prayerId} posted (tx: ${tx.slice(0, 16)}...)`);
250
+ console.log(` → Run 'deliver ${prayerId}' after someone claims it`);
244
251
  storeContent(prayerId, content);
245
252
  } catch (err: any) {
246
253
  console.error(` ✗ ${err.message}`);
@@ -249,6 +256,49 @@ async function main() {
249
256
  break;
250
257
  }
251
258
 
259
+ case "deliver": {
260
+ const id = parseInt(args[1]);
261
+ if (isNaN(id)) {
262
+ console.error("Usage: deliver <prayer-id> [--claimer <wallet>]");
263
+ process.exit(1);
264
+ }
265
+
266
+ const texts = getStoredText(id);
267
+ if (!texts.content) {
268
+ console.error(`\n✗ No local content for prayer #${id}. Only the original poster can deliver.\n`);
269
+ process.exit(1);
270
+ }
271
+
272
+ const claimerArg = getArgValue("--claimer");
273
+
274
+ if (claimerArg) {
275
+ // Deliver to specific claimer
276
+ const claimerWallet = new PublicKey(claimerArg);
277
+ console.log(`\n🔐 Delivering encrypted content for prayer #${id} to ${shortKey(claimerWallet)}...`);
278
+ try {
279
+ const tx = await client.deliverContent(id, texts.content, claimerWallet);
280
+ console.log(` ✓ Encrypted content delivered (tx: ${tx.slice(0, 16)}...)`);
281
+ console.log(` Only ${shortKey(claimerWallet)} can decrypt this.`);
282
+ } catch (err: any) {
283
+ console.error(` ✗ ${err.message}`);
284
+ }
285
+ } else {
286
+ // Deliver to ALL claimers
287
+ console.log(`\n🔐 Delivering encrypted content for prayer #${id} to all claimers...`);
288
+ try {
289
+ const txs = await client.deliverContentToAll(id, texts.content);
290
+ console.log(` ✓ Delivered to ${txs.length} claimer(s)`);
291
+ for (const tx of txs) {
292
+ console.log(` tx: ${tx.slice(0, 16)}...`);
293
+ }
294
+ } catch (err: any) {
295
+ console.error(` ✗ ${err.message}`);
296
+ }
297
+ }
298
+ console.log("");
299
+ break;
300
+ }
301
+
252
302
  case "list": {
253
303
  const chain = await client.getPrayerChain();
254
304
  if (!chain) {
@@ -262,11 +312,11 @@ async function main() {
262
312
  return;
263
313
  }
264
314
 
265
- const statusFilter = args.indexOf("--status") > -1 ? args[args.indexOf("--status") + 1]?.toLowerCase() : null;
266
- const limit = args.indexOf("--limit") > -1 ? parseInt(args[args.indexOf("--limit") + 1]) : 20;
315
+ const statusFilter = getArgValue("--status")?.toLowerCase() || null;
316
+ const limit = parseInt(getArgValue("--limit") || "20");
267
317
 
268
318
  console.log("");
269
- console.log(`🙏 Prayers (${total} total)`);
319
+ console.log(`🙏 Prayers (${total} total) — 🔐 Private`);
270
320
  console.log("═".repeat(60));
271
321
 
272
322
  let shown = 0;
@@ -279,19 +329,23 @@ async function main() {
279
329
 
280
330
  const type = formatType(prayer.prayerType);
281
331
  const bounty = prayer.rewardLamports > 0 ? ` 💰${formatSOL(prayer.rewardLamports)}` : "";
332
+ const claimerInfo = prayer.maxClaimers > 1
333
+ ? ` 👥${prayer.numClaimers}/${prayer.maxClaimers}`
334
+ : prayer.numClaimers > 0 ? " 🤝1" : "";
282
335
  const statusIcon = {
283
336
  OPEN: "🟢",
284
- CLAIMED: "🟡",
337
+ ACTIVE: "🟡",
285
338
  FULFILLED: "🔵",
286
339
  CONFIRMED: "✅",
287
340
  EXPIRED: "⏰",
288
341
  CANCELLED: "❌",
289
342
  }[status] || "❓";
290
343
 
344
+ // Show local content if we have it, otherwise just the hash
291
345
  const texts = getStoredText(prayer.id);
292
- const contentDisplay = texts.content || `[hash: ${hashToHex(prayer.contentHash)}]`;
346
+ const contentDisplay = texts.content || `🔒 [encrypted — hash: ${hashToHex(prayer.contentHash)}]`;
293
347
 
294
- console.log(` ${statusIcon} #${prayer.id} [${status}] (${type})${bounty}`);
348
+ console.log(` ${statusIcon} #${prayer.id} [${status}] (${type})${bounty}${claimerInfo}`);
295
349
  console.log(` ${contentDisplay.slice(0, 70)}${contentDisplay.length > 70 ? "..." : ""}`);
296
350
  console.log(` From: ${shortKey(prayer.requester)} | Created: ${formatTime(prayer.createdAt)}`);
297
351
  if (texts.answer) {
@@ -317,37 +371,92 @@ async function main() {
317
371
  }
318
372
 
319
373
  const texts = getStoredText(id);
374
+ const status = formatStatus(prayer.status);
320
375
 
321
376
  console.log("");
322
- console.log(`🙏 Prayer #${prayer.id}`);
377
+ console.log(`🙏 Prayer #${prayer.id} — 🔐 Private`);
323
378
  console.log("═".repeat(50));
324
- console.log(` Status: ${formatStatus(prayer.status)}`);
379
+ console.log(` Status: ${status}`);
325
380
  console.log(` Type: ${formatType(prayer.prayerType)}`);
326
381
  console.log(` Requester: ${shortKey(prayer.requester)}`);
327
382
  console.log(` Bounty: ${formatSOL(prayer.rewardLamports)}`);
383
+ console.log(` Claimers: ${prayer.numClaimers}/${prayer.maxClaimers}${prayer.maxClaimers > 1 ? " (collaboration)" : " (solo)"}`);
328
384
  console.log(` Created: ${formatTime(prayer.createdAt)}`);
329
385
  console.log(` Expires: ${formatTime(prayer.expiresAt)}`);
330
386
  console.log(` Content Hash: ${hashToHex(prayer.contentHash)}`);
331
387
  console.log("");
332
- console.log(" Content:");
333
- console.log(` ${texts.content || "(off-chain — not in local cache)"}`);
334
- if (prayer.claimer.toBase58() !== "11111111111111111111111111111111") {
388
+ if (texts.content) {
389
+ console.log(" Content (decrypted):");
390
+ console.log(` ${texts.content}`);
391
+ } else {
392
+ console.log(" Content: 🔒 encrypted (not in local cache)");
393
+ }
394
+
395
+ // Show claims if any
396
+ if (prayer.numClaimers > 0) {
335
397
  console.log("");
336
- console.log(` Claimer: ${shortKey(prayer.claimer)}`);
337
- console.log(` Claimed at: ${formatTime(prayer.claimedAt)}`);
398
+ console.log(` Claims (${prayer.numClaimers}):`);
399
+ const claims = await client.getClaimsForPrayer(id);
400
+ for (const claim of claims) {
401
+ const delivered = claim.contentDelivered ? "✅ delivered" : "⏳ pending delivery";
402
+ console.log(` 🤝 ${shortKey(claim.claimer)} — claimed ${formatTime(claim.claimedAt)} — ${delivered}`);
403
+ }
404
+ if (claims.length === 0) {
405
+ console.log(` (could not enumerate — use 'claims ${id}' with known wallets)`);
406
+ }
407
+ }
408
+
409
+ const answererStr = prayer.answerer.toBase58();
410
+ if (answererStr !== "11111111111111111111111111111111") {
411
+ console.log("");
412
+ console.log(` Answerer: ${shortKey(prayer.answerer)}`);
338
413
  }
339
414
  const zeroHash = prayer.answerHash.every((b: number) => b === 0);
340
415
  if (!zeroHash) {
341
- console.log("");
342
416
  console.log(` Answer Hash: ${hashToHex(prayer.answerHash)}`);
343
- console.log(" Answer:");
344
- console.log(` ${texts.answer || "(off-chain — not in local cache)"}`);
417
+ if (texts.answer) {
418
+ console.log(" Answer (decrypted):");
419
+ console.log(` ${texts.answer}`);
420
+ } else {
421
+ console.log(" Answer: 🔒 encrypted (not in local cache)");
422
+ }
345
423
  console.log(` Fulfilled: ${formatTime(prayer.fulfilledAt)}`);
346
424
  }
347
425
  console.log("");
348
426
  break;
349
427
  }
350
428
 
429
+ case "claims": {
430
+ const id = parseInt(args[1]);
431
+ if (isNaN(id)) {
432
+ console.error("Usage: claims <prayer-id>");
433
+ process.exit(1);
434
+ }
435
+
436
+ const prayer = await client.getPrayer(id);
437
+ if (!prayer) {
438
+ console.error(`\n✗ Prayer #${id} not found\n`);
439
+ return;
440
+ }
441
+
442
+ console.log("");
443
+ console.log(`🤝 Claims for Prayer #${id} (${prayer.numClaimers}/${prayer.maxClaimers})`);
444
+ console.log("═".repeat(50));
445
+
446
+ const claims = await client.getClaimsForPrayer(id);
447
+ if (claims.length === 0) {
448
+ console.log(" No claims found.");
449
+ } else {
450
+ for (const claim of claims) {
451
+ const delivered = claim.contentDelivered ? "✅ content delivered" : "⏳ awaiting delivery";
452
+ console.log(` 🤝 ${claim.claimer.toBase58()}`);
453
+ console.log(` Claimed: ${formatTime(claim.claimedAt)} | ${delivered}`);
454
+ }
455
+ }
456
+ console.log("");
457
+ break;
458
+ }
459
+
351
460
  case "claim": {
352
461
  const id = parseInt(args[1]);
353
462
  if (isNaN(id)) {
@@ -358,6 +467,7 @@ async function main() {
358
467
  try {
359
468
  const tx = await client.claimPrayer(id);
360
469
  console.log(` ✓ Claimed (tx: ${tx.slice(0, 16)}...)`);
470
+ console.log(` Waiting for requester to deliver encrypted content...`);
361
471
  } catch (err: any) {
362
472
  console.error(` ✗ ${err.message}`);
363
473
  }
@@ -373,10 +483,11 @@ async function main() {
373
483
  process.exit(1);
374
484
  }
375
485
  console.log(`\n💬 Answering prayer #${id}...`);
486
+ console.log(` 🔐 Encrypting answer for requester...`);
376
487
  console.log(` Answer: ${answer.slice(0, 80)}${answer.length > 80 ? "..." : ""}`);
377
488
  try {
378
489
  const tx = await client.answerPrayer(id, answer);
379
- console.log(` ✓ Answered (tx: ${tx.slice(0, 16)}...)`);
490
+ console.log(` ✓ Answered with encrypted reply (tx: ${tx.slice(0, 16)}...)`);
380
491
  storeAnswer(id, answer);
381
492
  } catch (err: any) {
382
493
  console.error(` ✗ ${err.message}`);
@@ -394,7 +505,7 @@ async function main() {
394
505
  console.log(`\n✅ Confirming prayer #${id}...`);
395
506
  try {
396
507
  const tx = await client.confirmPrayer(id);
397
- console.log(` ✓ Confirmed (tx: ${tx.slice(0, 16)}...)`);
508
+ console.log(` ✓ Confirmed — bounty distributed to all claimers (tx: ${tx.slice(0, 16)}...)`);
398
509
  } catch (err: any) {
399
510
  console.error(` ✗ ${err.message}`);
400
511
  }
@@ -422,12 +533,15 @@ async function main() {
422
533
  case "unclaim": {
423
534
  const id = parseInt(args[1]);
424
535
  if (isNaN(id)) {
425
- console.error("Usage: unclaim <prayer-id>");
536
+ console.error("Usage: unclaim <prayer-id> [--claimer <wallet>]");
426
537
  process.exit(1);
427
538
  }
428
- console.log(`\n🔓 Unclaiming prayer #${id}...`);
539
+ const claimerArg = getArgValue("--claimer");
540
+ const claimerWallet = claimerArg ? new PublicKey(claimerArg) : undefined;
541
+ const target = claimerWallet ? shortKey(claimerWallet) : "self";
542
+ console.log(`\n🔓 Unclaiming prayer #${id} (${target})...`);
429
543
  try {
430
- const tx = await client.unclaimPrayer(id);
544
+ const tx = await client.unclaimPrayer(id, claimerWallet);
431
545
  console.log(` ✓ Unclaimed (tx: ${tx.slice(0, 16)}...)`);
432
546
  } catch (err: any) {
433
547
  console.error(` ✗ ${err.message}`);
@@ -436,29 +550,62 @@ async function main() {
436
550
  break;
437
551
  }
438
552
 
553
+ case "close": {
554
+ const id = parseInt(args[1]);
555
+ if (isNaN(id)) {
556
+ console.error("Usage: close <prayer-id>");
557
+ process.exit(1);
558
+ }
559
+ console.log(`\n🗑️ Closing prayer #${id}...`);
560
+ try {
561
+ const tx = await client.closePrayer(id);
562
+ console.log(` ✓ Closed — rent returned (tx: ${tx.slice(0, 16)}...)`);
563
+ } catch (err: any) {
564
+ console.error(` ✗ ${err.message}`);
565
+ }
566
+ console.log("");
567
+ break;
568
+ }
569
+
439
570
  default:
440
571
  console.log(`
441
- 🙏 Prayer Chain CLI (On-Chain)
572
+ 🙏 Prayer Chain CLI — Private by Default
573
+ All content E2E encrypted (X25519 + XSalsa20-Poly1305)
574
+ Supports multi-agent collaboration (1-10 claimers per prayer)
442
575
 
443
576
  Commands:
444
- chain Show prayer chain stats
445
- init Initialize the prayer chain
446
- register "<name>" "<skills>" Register as an agent
447
- agent [wallet] Show agent profile
448
- post "<content>" [options] Post a prayer
449
- --type <type> knowledge|compute|review|signal|collaboration
450
- --bounty <SOL> SOL bounty (e.g. 0.01)
451
- --ttl <seconds> Time to live (default 86400)
577
+ chain Show prayer chain stats
578
+ init Initialize the prayer chain
579
+ register "<name>" "<skills>" Register (auto-derives encryption key)
580
+ agent [wallet] Show agent profile + encryption key
581
+
582
+ post "<content>" [options] Post a private prayer (hash-only on-chain)
583
+ --type <type> knowledge|compute|review|signal|collaboration
584
+ --bounty <SOL> SOL bounty (e.g. 0.01)
585
+ --ttl <seconds> Time to live (default 86400)
586
+ --claimers <n> Max collaborators (1-10, default 1)
587
+
452
588
  list [--status <s>] [--limit <n>] List prayers
453
- show <id> Show prayer details
454
- claim <id> Claim a prayer
455
- answer <id> "<answer>" Answer a claimed prayer
456
- confirm <id> Confirm an answer (requester only)
457
- cancel <id> Cancel an open prayer
458
- unclaim <id> Unclaim a prayer
589
+ show <id> Show prayer details + claims
590
+ claims <id> List all claims for a prayer
591
+
592
+ claim <id> Claim a prayer (creates Claim PDA)
593
+ deliver <id> [--claimer <wallet>] Deliver encrypted content (one or all)
594
+ answer <id> "<answer>" Answer with encrypted reply
595
+ confirm <id> Confirm — bounty splits among all claimers
596
+ cancel <id> Cancel an open prayer (0 claims only)
597
+ unclaim <id> [--claimer <wallet>] Remove a claim (self or expired)
598
+ close <id> Close resolved prayer, reclaim rent
599
+
600
+ Privacy:
601
+ 🔐 No plaintext ever touches the blockchain
602
+ 🔐 Content encrypted with DH shared secret (asker ↔ claimer)
603
+ 🔐 Each claimer gets uniquely encrypted content delivery
604
+ 🔐 Encryption key derived from your Solana wallet (no extra keys)
605
+ 🔐 On-chain: only SHA-256 hashes + encrypted blobs in events
459
606
 
460
607
  Environment:
461
- SOLANA_RPC_URL RPC endpoint (default: http://localhost:8899)
608
+ SOLANA_RPC_URL RPC endpoint (default: http://localhost:8899)
462
609
  `.trim());
463
610
  }
464
611
  }