@iamoberlin/chorus 1.3.9 → 2.0.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.
@@ -1,330 +1,469 @@
1
- #!/usr/bin/env npx ts-node
1
+ #!/usr/bin/env npx tsx
2
2
  /**
3
- * Prayer Requests - CLI
4
- * Simple command-line interface for testing
3
+ * Prayer Chain — On-Chain CLI
4
+ *
5
+ * Human-in-the-loop commands for the Solana prayer chain.
6
+ * Choirs suggest prayers; humans approve and send them on-chain.
5
7
  *
6
8
  * Usage:
7
- * npx ts-node cli.ts pray "Need research on X" --category research
8
- * npx ts-node cli.ts list [--status open] [--category research]
9
- * npx ts-node cli.ts show <id>
10
- * npx ts-node cli.ts accept <id>
11
- * npx ts-node cli.ts complete <id> "Here's the result..."
12
- * npx ts-node cli.ts confirm <id> [--reject]
13
- * npx ts-node cli.ts reputation [agentId]
14
- * npx ts-node cli.ts peers
15
- * npx ts-node cli.ts add-peer <id> <endpoint>
9
+ * chorus pray post "What is the current SOFR rate?" --type knowledge --bounty 0.01
10
+ * chorus pray list
11
+ * chorus pray show <id>
12
+ * chorus pray claim <id>
13
+ * chorus pray answer <id> "SOFR is 4.55%, down 2bps this week"
14
+ * chorus pray confirm <id>
15
+ * 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
16
19
  */
17
20
 
18
- import * as prayers from './prayers';
19
- import * as store from './store';
20
- import type { PrayerCategory } from './types';
21
+ import { ChorusPrayerClient, PrayerType, getPrayerChainPDA, getAgentPDA, getPrayerPDA } from "./solana.js";
22
+ import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
23
+ import { createHash } from "crypto";
24
+ import * as fs from "fs";
25
+ import * as path from "path";
26
+ import { fileURLToPath } from "url";
21
27
 
22
- const args = process.argv.slice(2);
23
- const command = args[0];
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+
31
+ // Default to localhost; override with SOLANA_RPC_URL env
32
+ const RPC_URL = process.env.SOLANA_RPC_URL || "http://localhost:8899";
33
+
34
+ // ── Local text store (off-chain content cache) ────────────
35
+ const STORE_PATH = path.join(__dirname, "../../.prayer-texts.json");
36
+
37
+ interface TextStore {
38
+ [prayerId: string]: { content?: string; answer?: string };
39
+ }
24
40
 
25
- function formatDate(ts: number): string {
26
- return new Date(ts).toLocaleString();
41
+ function loadTextStore(): TextStore {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(STORE_PATH, "utf-8"));
44
+ } catch {
45
+ return {};
46
+ }
27
47
  }
28
48
 
29
- function formatRequest(r: ReturnType<typeof prayers.getRequest>) {
30
- if (!r) return 'Not found';
31
- const { request: req, responses } = r;
32
-
33
- return `
34
- ID: ${req.id}
35
- From: ${req.from.name || req.from.id}
36
- Type: ${req.type}
37
- Category: ${req.category}
38
- Status: ${req.status}
39
- Created: ${formatDate(req.createdAt)}
40
- ${req.expiresAt ? `Expires: ${formatDate(req.expiresAt)}` : ''}
41
- ${req.deadline ? `Deadline: ${formatDate(req.deadline)}` : ''}
42
- ${req.reward ? `Reward: ${req.reward.amount} ${req.reward.token}` : ''}
43
- ${req.acceptedBy ? `Accepted: ${req.acceptedBy.name || req.acceptedBy.id}` : ''}
44
-
45
- Title:
46
- ${req.title}
47
-
48
- Content:
49
- ${req.content}
50
-
51
- Responses: ${responses.length}
52
- ${responses.map(r => ` - [${r.action}] ${r.from.name || r.from.id} @ ${formatDate(r.createdAt)}${r.result ? `\n Result: ${r.result.slice(0, 100)}...` : ''}`).join('\n')}
53
- `.trim();
49
+ function saveTextStore(store: TextStore): void {
50
+ fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
54
51
  }
55
52
 
56
- async function main() {
57
- // Set agent identity from env
58
- if (process.env.AGENT_ID) {
59
- prayers.setSelf({
60
- id: process.env.AGENT_ID,
61
- address: process.env.AGENT_ADDRESS || '0x0',
62
- name: process.env.AGENT_NAME,
63
- endpoint: process.env.AGENT_ENDPOINT
64
- });
53
+ function storeContent(prayerId: number, content: string): void {
54
+ const store = loadTextStore();
55
+ if (!store[prayerId]) store[prayerId] = {};
56
+ store[prayerId].content = content;
57
+ saveTextStore(store);
58
+ }
59
+
60
+ function storeAnswer(prayerId: number, answer: string): void {
61
+ const store = loadTextStore();
62
+ if (!store[prayerId]) store[prayerId] = {};
63
+ store[prayerId].answer = answer;
64
+ saveTextStore(store);
65
+ }
66
+
67
+ function getStoredText(prayerId: number): { content?: string; answer?: string } {
68
+ const store = loadTextStore();
69
+ return store[prayerId] || {};
70
+ }
71
+
72
+ function hashToHex(hash: number[]): string {
73
+ return Buffer.from(hash).toString("hex").slice(0, 16) + "…";
74
+ }
75
+
76
+ function getClient(): ChorusPrayerClient {
77
+ return ChorusPrayerClient.fromDefaultKeypair(RPC_URL);
78
+ }
79
+
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
+ function formatStatus(status: any): string {
96
+ if (typeof status === "object") {
97
+ return Object.keys(status)[0].toUpperCase();
98
+ }
99
+ return String(status).toUpperCase();
100
+ }
101
+
102
+ function formatType(prayerType: any): string {
103
+ if (typeof prayerType === "object") {
104
+ return Object.keys(prayerType)[0];
65
105
  }
66
-
106
+ return String(prayerType);
107
+ }
108
+
109
+ function formatTime(ts: number): string {
110
+ if (!ts) return "—";
111
+ return new Date(ts * 1000).toLocaleString();
112
+ }
113
+
114
+ function formatSOL(lamports: number): string {
115
+ if (!lamports) return "none";
116
+ return `${(lamports / LAMPORTS_PER_SOL).toFixed(4)} SOL`;
117
+ }
118
+
119
+ function shortKey(key: PublicKey): string {
120
+ const s = key.toBase58();
121
+ if (s === "11111111111111111111111111111111") return "(none)";
122
+ return `${s.slice(0, 4)}...${s.slice(-4)}`;
123
+ }
124
+
125
+ const args = process.argv.slice(2);
126
+ const command = args[0];
127
+
128
+ async function main() {
129
+ const client = getClient();
130
+
67
131
  switch (command) {
68
- case 'whoami': {
69
- const self = prayers.getSelf();
70
- console.log(`ID: ${self.id}`);
71
- console.log(`Name: ${self.name}`);
72
- console.log(`Address: ${self.address}`);
132
+ case "chain":
133
+ case "status": {
134
+ const chain = await client.getPrayerChain();
135
+ if (!chain) {
136
+ console.log("\n⛓️ Prayer Chain not initialized.");
137
+ console.log(" Run: chorus pray init\n");
138
+ return;
139
+ }
140
+ console.log("");
141
+ console.log("⛓️ Prayer Chain");
142
+ console.log("═".repeat(40));
143
+ console.log(` Authority: ${shortKey(chain.authority)}`);
144
+ console.log(` Total Prayers: ${chain.totalPrayers}`);
145
+ console.log(` Total Answered: ${chain.totalAnswered}`);
146
+ console.log(` Total Agents: ${chain.totalAgents}`);
147
+ console.log(` RPC: ${RPC_URL}`);
148
+ console.log("");
73
149
  break;
74
150
  }
75
-
76
- case 'pray': {
77
- const content = args[1];
78
- if (!content) {
79
- console.error('Usage: pray "<content>" [--category <cat>] [--title <title>]');
80
- process.exit(1);
151
+
152
+ case "init": {
153
+ console.log("\n⛓️ Initializing Prayer Chain...");
154
+ try {
155
+ const tx = await client.initialize();
156
+ console.log(` ✓ Initialized (tx: ${tx.slice(0, 16)}...)`);
157
+ } catch (err: any) {
158
+ if (err.message?.includes("already in use")) {
159
+ console.log(" Already initialized.");
160
+ } else {
161
+ console.error(` ✗ ${err.message}`);
162
+ }
81
163
  }
82
-
83
- const categoryIdx = args.indexOf('--category');
84
- const titleIdx = args.indexOf('--title');
85
-
86
- const category = (categoryIdx > -1 ? args[categoryIdx + 1] : 'other') as PrayerCategory;
87
- const title = titleIdx > -1 ? args[titleIdx + 1] : content.slice(0, 50);
88
-
89
- const request = prayers.createRequest({
90
- type: 'ask',
91
- category,
92
- title,
93
- content,
94
- expiresIn: 24 * 60 * 60 * 1000 // 24 hours
95
- });
96
-
97
- console.log(`Created prayer request: ${request.id}`);
98
- console.log(`Title: ${request.title}`);
99
- console.log(`Status: ${request.status}`);
164
+ console.log("");
100
165
  break;
101
166
  }
102
-
103
- case 'offer': {
104
- const content = args[1];
105
- if (!content) {
106
- console.error('Usage: offer "<what you offer>" [--category <cat>]');
167
+
168
+ case "register": {
169
+ const name = args[1];
170
+ const skills = args[2];
171
+ if (!name || !skills) {
172
+ console.error('Usage: register "<name>" "<skills>"');
107
173
  process.exit(1);
108
174
  }
109
-
110
- const categoryIdx = args.indexOf('--category');
111
- const category = (categoryIdx > -1 ? args[categoryIdx + 1] : 'other') as PrayerCategory;
112
-
113
- const request = prayers.createRequest({
114
- type: 'offer',
115
- category,
116
- title: content.slice(0, 50),
117
- content
118
- });
119
-
120
- console.log(`Created offer: ${request.id}`);
175
+ console.log(`\n🤖 Registering agent "${name}"...`);
176
+ try {
177
+ const tx = await client.registerAgent(name, skills);
178
+ console.log(` ✓ Registered (tx: ${tx.slice(0, 16)}...)`);
179
+ } catch (err: any) {
180
+ if (err.message?.includes("already in use")) {
181
+ console.log(" Already registered.");
182
+ } else {
183
+ console.error(` ✗ ${err.message}`);
184
+ }
185
+ }
186
+ console.log("");
121
187
  break;
122
188
  }
123
-
124
- case 'list': {
125
- const statusIdx = args.indexOf('--status');
126
- const categoryIdx = args.indexOf('--category');
127
- const mineFlag = args.includes('--mine');
128
-
129
- const requests = prayers.listRequests({
130
- status: statusIdx > -1 ? args[statusIdx + 1] as any : undefined,
131
- category: categoryIdx > -1 ? args[categoryIdx + 1] as PrayerCategory : undefined,
132
- mine: mineFlag
133
- });
134
-
135
- if (requests.length === 0) {
136
- console.log('No prayer requests found.');
137
- break;
138
- }
139
-
140
- console.log(`Found ${requests.length} request(s):\n`);
141
- for (const req of requests) {
142
- console.log(`[${req.status.toUpperCase()}] ${req.id.slice(0, 8)}...`);
143
- console.log(` ${req.type === 'ask' ? '🙏' : '✋'} ${req.title}`);
144
- console.log(` From: ${req.from.name || req.from.id} | Category: ${req.category}`);
145
- console.log(` Created: ${formatDate(req.createdAt)}`);
146
- console.log();
189
+
190
+ case "agent": {
191
+ const wallet = args[1] ? new PublicKey(args[1]) : client.wallet;
192
+ const agent = await client.getAgent(wallet);
193
+ if (!agent) {
194
+ console.log("\n🤖 Agent not registered.");
195
+ console.log(' Run: chorus pray register "<name>" "<skills>"\n');
196
+ return;
147
197
  }
198
+ console.log("");
199
+ console.log("🤖 Agent");
200
+ 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(` Prayers Answered: ${agent.prayersAnswered}`);
207
+ console.log(` Prayers Confirmed: ${agent.prayersConfirmed}`);
208
+ console.log(` Registered: ${formatTime(agent.registeredAt)}`);
209
+ console.log("");
148
210
  break;
149
211
  }
150
-
151
- case 'show': {
152
- const id = args[1];
153
- if (!id) {
154
- console.error('Usage: show <request-id>');
212
+
213
+ case "post": {
214
+ const content = args[1];
215
+ if (!content) {
216
+ console.error('Usage: post "<content>" [--type knowledge] [--bounty 0.01] [--ttl 86400]');
155
217
  process.exit(1);
156
218
  }
157
-
158
- // Support partial ID match
159
- const all = prayers.listRequests({});
160
- const match = all.find(r => r.id.startsWith(id));
161
-
162
- if (!match) {
163
- console.error('Request not found');
164
- process.exit(1);
219
+
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;
227
+ const bountyLamports = Math.round(bountySOL * LAMPORTS_PER_SOL);
228
+
229
+ 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)`);
235
+
236
+ try {
237
+ const { tx, prayerId } = await client.postPrayer(
238
+ prayerType as unknown as PrayerType,
239
+ content,
240
+ bountyLamports,
241
+ ttl
242
+ );
243
+ console.log(` ✓ Prayer #${prayerId} posted (tx: ${tx.slice(0, 16)}...)`);
244
+ storeContent(prayerId, content);
245
+ } catch (err: any) {
246
+ console.error(` ✗ ${err.message}`);
165
247
  }
166
-
167
- console.log(formatRequest(prayers.getRequest(match.id)));
248
+ console.log("");
168
249
  break;
169
250
  }
170
-
171
- case 'accept': {
172
- const id = args[1];
173
- if (!id) {
174
- console.error('Usage: accept <request-id>');
175
- process.exit(1);
251
+
252
+ case "list": {
253
+ const chain = await client.getPrayerChain();
254
+ if (!chain) {
255
+ console.log("\n⛓️ Prayer Chain not initialized.\n");
256
+ return;
176
257
  }
177
-
178
- const all = prayers.listRequests({});
179
- const match = all.find(r => r.id.startsWith(id));
180
-
181
- if (!match) {
182
- console.error('Request not found');
183
- process.exit(1);
258
+
259
+ const total = chain.totalPrayers;
260
+ if (total === 0) {
261
+ console.log("\n🙏 No prayers yet.\n");
262
+ return;
184
263
  }
185
-
186
- const response = prayers.acceptRequest(match.id);
187
- if (response) {
188
- console.log(`Accepted request: ${match.id}`);
189
- console.log(`Response ID: ${response.id}`);
190
- } else {
191
- console.error('Could not accept request (may be expired or already accepted)');
192
- process.exit(1);
264
+
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;
267
+
268
+ console.log("");
269
+ console.log(`🙏 Prayers (${total} total)`);
270
+ console.log("═".repeat(60));
271
+
272
+ let shown = 0;
273
+ for (let i = total - 1; i >= 0 && shown < limit; i--) {
274
+ const prayer = await client.getPrayer(i);
275
+ if (!prayer) continue;
276
+
277
+ const status = formatStatus(prayer.status);
278
+ if (statusFilter && status.toLowerCase() !== statusFilter) continue;
279
+
280
+ const type = formatType(prayer.prayerType);
281
+ const bounty = prayer.rewardLamports > 0 ? ` 💰${formatSOL(prayer.rewardLamports)}` : "";
282
+ const statusIcon = {
283
+ OPEN: "🟢",
284
+ CLAIMED: "🟡",
285
+ FULFILLED: "🔵",
286
+ CONFIRMED: "✅",
287
+ EXPIRED: "⏰",
288
+ CANCELLED: "❌",
289
+ }[status] || "❓";
290
+
291
+ const texts = getStoredText(prayer.id);
292
+ const contentDisplay = texts.content || `[hash: ${hashToHex(prayer.contentHash)}]`;
293
+
294
+ console.log(` ${statusIcon} #${prayer.id} [${status}] (${type})${bounty}`);
295
+ console.log(` ${contentDisplay.slice(0, 70)}${contentDisplay.length > 70 ? "..." : ""}`);
296
+ console.log(` From: ${shortKey(prayer.requester)} | Created: ${formatTime(prayer.createdAt)}`);
297
+ if (texts.answer) {
298
+ console.log(` 💬 ${texts.answer.slice(0, 70)}${texts.answer.length > 70 ? "..." : ""}`);
299
+ }
300
+ shown++;
193
301
  }
302
+ console.log("");
194
303
  break;
195
304
  }
196
-
197
- case 'complete': {
198
- const id = args[1];
199
- const result = args[2];
200
- if (!id || !result) {
201
- console.error('Usage: complete <request-id> "<result>"');
305
+
306
+ case "show": {
307
+ const id = parseInt(args[1]);
308
+ if (isNaN(id)) {
309
+ console.error("Usage: show <prayer-id>");
202
310
  process.exit(1);
203
311
  }
204
-
205
- const all = prayers.listRequests({});
206
- const match = all.find(r => r.id.startsWith(id));
207
-
208
- if (!match) {
209
- console.error('Request not found');
210
- process.exit(1);
312
+
313
+ const prayer = await client.getPrayer(id);
314
+ if (!prayer) {
315
+ console.error(`\n✗ Prayer #${id} not found\n`);
316
+ return;
211
317
  }
212
-
213
- const response = prayers.completeRequest(match.id, result);
214
- if (response) {
215
- console.log(`Marked complete: ${match.id}`);
216
- console.log(`Awaiting confirmation from requester`);
217
- } else {
218
- console.error('Could not complete (not accepted by you?)');
219
- process.exit(1);
318
+
319
+ const texts = getStoredText(id);
320
+
321
+ console.log("");
322
+ console.log(`🙏 Prayer #${prayer.id}`);
323
+ console.log("═".repeat(50));
324
+ console.log(` Status: ${formatStatus(prayer.status)}`);
325
+ console.log(` Type: ${formatType(prayer.prayerType)}`);
326
+ console.log(` Requester: ${shortKey(prayer.requester)}`);
327
+ console.log(` Bounty: ${formatSOL(prayer.rewardLamports)}`);
328
+ console.log(` Created: ${formatTime(prayer.createdAt)}`);
329
+ console.log(` Expires: ${formatTime(prayer.expiresAt)}`);
330
+ console.log(` Content Hash: ${hashToHex(prayer.contentHash)}`);
331
+ console.log("");
332
+ console.log(" Content:");
333
+ console.log(` ${texts.content || "(off-chain — not in local cache)"}`);
334
+ if (prayer.claimer.toBase58() !== "11111111111111111111111111111111") {
335
+ console.log("");
336
+ console.log(` Claimer: ${shortKey(prayer.claimer)}`);
337
+ console.log(` Claimed at: ${formatTime(prayer.claimedAt)}`);
220
338
  }
339
+ const zeroHash = prayer.answerHash.every((b: number) => b === 0);
340
+ if (!zeroHash) {
341
+ console.log("");
342
+ console.log(` Answer Hash: ${hashToHex(prayer.answerHash)}`);
343
+ console.log(" Answer:");
344
+ console.log(` ${texts.answer || "(off-chain — not in local cache)"}`);
345
+ console.log(` Fulfilled: ${formatTime(prayer.fulfilledAt)}`);
346
+ }
347
+ console.log("");
221
348
  break;
222
349
  }
223
-
224
- case 'confirm': {
225
- const id = args[1];
226
- const reject = args.includes('--reject');
227
-
228
- if (!id) {
229
- console.error('Usage: confirm <request-id> [--reject]');
350
+
351
+ case "claim": {
352
+ const id = parseInt(args[1]);
353
+ if (isNaN(id)) {
354
+ console.error("Usage: claim <prayer-id>");
230
355
  process.exit(1);
231
356
  }
232
-
233
- const all = prayers.listRequests({});
234
- const match = all.find(r => r.id.startsWith(id));
235
-
236
- if (!match) {
237
- console.error('Request not found');
238
- process.exit(1);
357
+ console.log(`\n🤝 Claiming prayer #${id}...`);
358
+ try {
359
+ const tx = await client.claimPrayer(id);
360
+ console.log(` ✓ Claimed (tx: ${tx.slice(0, 16)}...)`);
361
+ } catch (err: any) {
362
+ console.error(` ✗ ${err.message}`);
239
363
  }
240
-
241
- const detail = prayers.getRequest(match.id);
242
- const completion = detail?.responses.find(r => r.action === 'complete');
243
-
244
- if (!completion) {
245
- console.error('No completion to confirm');
364
+ console.log("");
365
+ break;
366
+ }
367
+
368
+ case "answer": {
369
+ const id = parseInt(args[1]);
370
+ const answer = args[2];
371
+ if (isNaN(id) || !answer) {
372
+ console.error('Usage: answer <prayer-id> "<answer>"');
246
373
  process.exit(1);
247
374
  }
248
-
249
- const confirmation = prayers.confirmCompletion(match.id, completion.id, !reject);
250
- if (confirmation) {
251
- console.log(reject ? 'Disputed completion' : 'Confirmed completion');
252
- console.log(`Request status: ${reject ? 'disputed' : 'completed'}`);
253
- } else {
254
- console.error('Could not confirm (not your request?)');
255
- process.exit(1);
375
+ console.log(`\n💬 Answering prayer #${id}...`);
376
+ console.log(` Answer: ${answer.slice(0, 80)}${answer.length > 80 ? "..." : ""}`);
377
+ try {
378
+ const tx = await client.answerPrayer(id, answer);
379
+ console.log(` Answered (tx: ${tx.slice(0, 16)}...)`);
380
+ storeAnswer(id, answer);
381
+ } catch (err: any) {
382
+ console.error(` ✗ ${err.message}`);
256
383
  }
384
+ console.log("");
257
385
  break;
258
386
  }
259
-
260
- case 'reputation': {
261
- const agentId = args[1];
262
- const rep = prayers.getReputation(agentId);
263
-
264
- console.log(`Agent: ${rep.agentId}`);
265
- console.log(`Fulfilled: ${rep.fulfilled}`);
266
- console.log(`Requested: ${rep.requested}`);
267
- console.log(`Disputed: ${rep.disputed}`);
268
- console.log(`Last Active: ${rep.lastActive ? formatDate(rep.lastActive) : 'Never'}`);
387
+
388
+ case "confirm": {
389
+ const id = parseInt(args[1]);
390
+ if (isNaN(id)) {
391
+ console.error("Usage: confirm <prayer-id>");
392
+ process.exit(1);
393
+ }
394
+ console.log(`\n✅ Confirming prayer #${id}...`);
395
+ try {
396
+ const tx = await client.confirmPrayer(id);
397
+ console.log(` ✓ Confirmed (tx: ${tx.slice(0, 16)}...)`);
398
+ } catch (err: any) {
399
+ console.error(` ✗ ${err.message}`);
400
+ }
401
+ console.log("");
269
402
  break;
270
403
  }
271
-
272
- case 'peers': {
273
- const peers = store.getPeers();
274
- if (peers.length === 0) {
275
- console.log('No peers configured');
276
- break;
404
+
405
+ case "cancel": {
406
+ const id = parseInt(args[1]);
407
+ if (isNaN(id)) {
408
+ console.error("Usage: cancel <prayer-id>");
409
+ process.exit(1);
277
410
  }
278
-
279
- console.log(`Known peers (${peers.length}):\n`);
280
- for (const peer of peers) {
281
- console.log(` ${peer.name || peer.id}`);
282
- console.log(` ID: ${peer.id}`);
283
- console.log(` Endpoint: ${peer.endpoint || 'none'}`);
284
- console.log();
411
+ console.log(`\n❌ Cancelling prayer #${id}...`);
412
+ try {
413
+ const tx = await client.cancelPrayer(id);
414
+ console.log(` ✓ Cancelled (tx: ${tx.slice(0, 16)}...)`);
415
+ } catch (err: any) {
416
+ console.error(` ${err.message}`);
285
417
  }
418
+ console.log("");
286
419
  break;
287
420
  }
288
-
289
- case 'add-peer': {
290
- const id = args[1];
291
- const endpoint = args[2];
292
- const name = args[3];
293
-
294
- if (!id) {
295
- console.error('Usage: add-peer <id> [endpoint] [name]');
421
+
422
+ case "unclaim": {
423
+ const id = parseInt(args[1]);
424
+ if (isNaN(id)) {
425
+ console.error("Usage: unclaim <prayer-id>");
296
426
  process.exit(1);
297
427
  }
298
-
299
- store.addPeer({ id, endpoint, address: '0x0', name });
300
- console.log(`Added peer: ${name || id}`);
428
+ console.log(`\n🔓 Unclaiming prayer #${id}...`);
429
+ try {
430
+ const tx = await client.unclaimPrayer(id);
431
+ console.log(` ✓ Unclaimed (tx: ${tx.slice(0, 16)}...)`);
432
+ } catch (err: any) {
433
+ console.error(` ✗ ${err.message}`);
434
+ }
435
+ console.log("");
301
436
  break;
302
437
  }
303
-
438
+
304
439
  default:
305
440
  console.log(`
306
- Prayer Requests CLI
441
+ 🙏 Prayer Chain CLI (On-Chain)
307
442
 
308
443
  Commands:
309
- whoami Show current agent identity
310
- pray "<content>" Create a prayer request (ask)
311
- offer "<content>" Create an offer
312
- list [--status X] [--mine] List requests
313
- show <id> Show request details
314
- accept <id> Accept a request
315
- complete <id> "<result>" Mark request complete
316
- confirm <id> [--reject] Confirm/reject completion
317
- reputation [agentId] Show reputation
318
- peers List known peers
319
- add-peer <id> [endpoint] [name] Add a peer
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)
452
+ 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
320
459
 
321
460
  Environment:
322
- AGENT_ID Your agent ID
323
- AGENT_NAME Your agent name
324
- AGENT_ADDRESS Your signing address
325
- AGENT_ENDPOINT Your gateway URL
461
+ SOLANA_RPC_URL RPC endpoint (default: http://localhost:8899)
326
462
  `.trim());
327
463
  }
328
464
  }
329
465
 
330
- main().catch(console.error);
466
+ main().catch((err) => {
467
+ console.error(`\n✗ ${err.message}\n`);
468
+ process.exit(1);
469
+ });