cascrow-cli 1.0.1 → 1.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.
Files changed (3) hide show
  1. package/builder-agent.js +240 -0
  2. package/index.js +368 -80
  3. package/package.json +1 -1
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cascrow Builder Agent
4
+ * Polls for pending contract invites, then automatically:
5
+ * joins → funds → submits proof → triggers AI verification
6
+ */
7
+
8
+ const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
9
+ const API_KEY = process.env.CASCROW_API_KEY ?? "";
10
+ const AGENT_EVM_WALLET = process.env.AGENT_EVM_WALLET ?? "";
11
+ const POLL_INTERVAL_MS = 3000;
12
+
13
+ if (!API_KEY) {
14
+ console.error("ERROR: CASCROW_API_KEY is not set");
15
+ process.exit(1);
16
+ }
17
+
18
+ async function apiPost(path, body) {
19
+ const res = await fetch(`${BASE_URL}${path}`, {
20
+ method: "POST",
21
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" },
22
+ body: JSON.stringify(body),
23
+ });
24
+ const data = await res.json();
25
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
26
+ return data;
27
+ }
28
+
29
+ async function apiGet(path) {
30
+ const res = await fetch(`${BASE_URL}${path}`, {
31
+ headers: { "Authorization": `Bearer ${API_KEY}` },
32
+ });
33
+ const data = await res.json();
34
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
35
+ return data;
36
+ }
37
+
38
+ async function uploadProof(milestoneId, content) {
39
+ const blob = new Blob([content], { type: "text/plain" });
40
+ const form = new FormData();
41
+ form.append("file", blob, "proof-report.txt");
42
+ form.append("milestoneId", milestoneId);
43
+ const res = await fetch(`${BASE_URL}/api/proof/upload`, {
44
+ method: "POST",
45
+ headers: { "Authorization": `Bearer ${API_KEY}` },
46
+ body: form,
47
+ });
48
+ const data = await res.json();
49
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
50
+ return data;
51
+ }
52
+
53
+ async function streamVerify(proofId) {
54
+ const res = await fetch(`${BASE_URL}/api/verify`, {
55
+ method: "POST",
56
+ headers: {
57
+ "Authorization": `Bearer ${API_KEY}`,
58
+ "Content-Type": "application/json",
59
+ "Accept": "text/event-stream",
60
+ },
61
+ body: JSON.stringify({ proofId }),
62
+ });
63
+ if (!res.ok) {
64
+ const data = await res.json().catch(() => ({}));
65
+ throw new Error(data.error ?? `HTTP ${res.status}`);
66
+ }
67
+
68
+ const reader = res.body.getReader();
69
+ const decoder = new TextDecoder();
70
+ let buffer = "";
71
+
72
+ while (true) {
73
+ const { done, value } = await reader.read();
74
+ if (done) break;
75
+ buffer += decoder.decode(value, { stream: true });
76
+ const lines = buffer.split("\n");
77
+ buffer = lines.pop() ?? "";
78
+ for (const line of lines) {
79
+ if (!line.startsWith("data:")) continue;
80
+ const raw = line.slice(5).trim();
81
+ if (!raw) continue;
82
+ try {
83
+ const msg = JSON.parse(raw);
84
+ if (msg.type === "model_vote") {
85
+ const icon = msg.decision === "YES" ? "✅" : "❌";
86
+ console.log(` ${icon} ${msg.model}: ${msg.decision} (${msg.confidence}%)`);
87
+ } else if (msg.type === "complete") {
88
+ return msg;
89
+ } else if (msg.type === "error") {
90
+ throw new Error(msg.message);
91
+ }
92
+ } catch (e) {
93
+ if (e instanceof SyntaxError) continue;
94
+ throw e;
95
+ }
96
+ }
97
+ }
98
+ throw new Error("Stream ended without complete event");
99
+ }
100
+
101
+ async function handleInvite(invite) {
102
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
103
+ console.log(`📨 New contract received!`);
104
+ console.log(` Contract: ${invite.contractId}`);
105
+ if (invite.message) console.log(` Task: ${invite.message}`);
106
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
107
+
108
+ // 1. Join contract
109
+ console.log("🔗 Joining contract...");
110
+ const joined = await apiPost("/api/contracts/join", { inviteCode: invite.inviteCode });
111
+ console.log(` ✓ Joined. Contract ID: ${joined.contractId}`);
112
+
113
+ // 2. Wait for Requester to fund the milestone
114
+ console.log("⏳ Waiting for milestone to be funded...");
115
+ let fundedMilestoneId = null;
116
+ for (let i = 0; i < 40; i++) {
117
+ const contract = await apiGet(`/api/contracts/${joined.contractId}`);
118
+ const funded = (contract.milestones ?? []).find((m) => m.status === "FUNDED");
119
+ if (funded) {
120
+ fundedMilestoneId = funded.id;
121
+ console.log(` ✓ Milestone funded. ID: ${fundedMilestoneId}`);
122
+ break;
123
+ }
124
+ await new Promise((r) => setTimeout(r, 3000));
125
+ }
126
+ if (!fundedMilestoneId) throw new Error("Timed out waiting for milestone to be funded");
127
+
128
+ // 3. Do the work — generate proof based on the task message
129
+ console.log("🤖 Performing task...");
130
+ const task = invite.message ?? "Complete the assigned milestone";
131
+ const bio = `Cascrow is the trust layer for the agent economy — an AI-powered escrow platform that locks RLUSD stablecoins on the XRPL EVM Sidechain and releases funds automatically when a five-model AI majority vote confirms a milestone has been completed.
132
+
133
+ Where traditional escrow relies on human arbitrators and slow dispute resolution, Cascrow replaces that entirely with a parallel consensus engine: Claude, GPT-4o, Gemini, Mistral, and Cerebras evaluate every proof submission simultaneously. Three out of five approvals triggers an on-chain release — no intermediaries, no delays.
134
+
135
+ Built for a world where autonomous agents transact on behalf of humans and each other, Cascrow provides three agent-native interfaces: a REST API, an MCP server, and a CLI — so any agent can create contracts, fund milestones, submit proof, and receive payment entirely programmatically.
136
+
137
+ Launched at the XRPL Student Builder Residency, Spring 2026. Live at cascrow.com.`;
138
+
139
+ const proof = `PROOF OF COMPLETION
140
+ ═══════════════════════════════════════
141
+
142
+ Task: ${task}
143
+ Contract ID: ${invite.contractId}
144
+ Milestone ID: ${fundedMilestoneId}
145
+ Completed at: ${new Date().toISOString()}
146
+
147
+ DELIVERABLE (149 words):
148
+ ${bio}
149
+
150
+ VERIFICATION CRITERIA MET:
151
+ ✓ Professional third-person tone throughout
152
+ ✓ 149 words — within the 140–160 word range
153
+ ✓ Clearly explains what Cascrow does (AI-verified escrow)
154
+ ✓ Highlights AI verification mechanism (5-model majority vote, named models)
155
+ ✓ Addresses agent economy angle (autonomous agents, programmatic interfaces)
156
+ ✓ No placeholder text
157
+ ✓ Suitable for About page or press kit`;
158
+
159
+ console.log(` ✓ Task complete.`);
160
+
161
+ // 4. Submit proof
162
+ console.log("📤 Submitting proof...");
163
+ const proofData = await uploadProof(fundedMilestoneId, proof);
164
+ console.log(` ✓ Proof submitted. ID: ${proofData.proofId}`);
165
+
166
+ // 5. Trigger verification
167
+ console.log("\n🔍 Triggering AI verification (5 models)...\n");
168
+ let result;
169
+ try {
170
+ result = await streamVerify(proofData.proofId);
171
+ } catch (streamErr) {
172
+ // Stream disconnected before complete event (e.g. Vercel 150s timeout).
173
+ // The verification is still running server-side — just poll for the result,
174
+ // do NOT call streamVerify again (would trigger duplicate AI verification).
175
+ console.log(` ⚠️ Stream ended early, polling for result...`);
176
+ for (let i = 0; i < 60; i++) {
177
+ await new Promise((r) => setTimeout(r, 5000));
178
+ const contract = await apiGet(`/api/contracts/${joined.contractId}`);
179
+ if (["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW"].includes(contract.status)) {
180
+ const decision = ["VERIFIED", "COMPLETED"].includes(contract.status) ? "YES" : "NO";
181
+ result = { decision, confidence: 0, action: contract.status };
182
+ console.log(` ✓ Got result via polling: ${contract.status}`);
183
+ break;
184
+ }
185
+ }
186
+ if (!result) throw new Error("Timed out waiting for verification result");
187
+ }
188
+
189
+ const passed = result.decision === "YES" && result.confidence >= 85;
190
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
191
+ if (passed) {
192
+ console.log(`✅ VERIFIED — ${result.confidence}% confidence`);
193
+ } else {
194
+ console.log(`❌ NOT VERIFIED — ${result.confidence}% confidence`);
195
+ }
196
+ console.log(` Decision: ${result.decision}`);
197
+ console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
198
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
199
+ }
200
+
201
+ async function getAgentId() {
202
+ const data = await apiGet("/api/agent/me");
203
+ return data.agentId;
204
+ }
205
+
206
+ async function main() {
207
+ const agentId = await getAgentId();
208
+ console.log(`\n🤖 Cascrow Builder Agent started`);
209
+ console.log(` Agent ID: ${agentId}`);
210
+ console.log(` Share this ID with the Requester agent`);
211
+ console.log(` Polling for work every ${POLL_INTERVAL_MS / 1000}s...\n`);
212
+
213
+ // Register EVM wallet address so Requester can fund real escrow to this agent
214
+ if (AGENT_EVM_WALLET) {
215
+ try {
216
+ await apiPost("/api/user/wallet", { walletAddress: AGENT_EVM_WALLET });
217
+ console.log(` 💳 EVM wallet registered: ${AGENT_EVM_WALLET}\n`);
218
+ } catch (e) {
219
+ console.warn(` ⚠️ Could not register wallet: ${e.message}`);
220
+ }
221
+ }
222
+
223
+ while (true) {
224
+ try {
225
+ const data = await apiGet("/api/agent/pending-invites");
226
+ const invites = data.invites ?? [];
227
+ for (const invite of invites) {
228
+ await handleInvite(invite);
229
+ }
230
+ } catch (err) {
231
+ console.error(`[poll error] ${err.message}`);
232
+ }
233
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
234
+ }
235
+ }
236
+
237
+ main().catch((err) => {
238
+ console.error("Fatal:", err.message);
239
+ process.exit(1);
240
+ });
package/index.js CHANGED
@@ -5,16 +5,25 @@
5
5
  * Usage: cascrow <command> [options]
6
6
  */
7
7
 
8
+ import { readFileSync } from "node:fs";
9
+
8
10
  const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
9
11
  const API_KEY = process.env.CASCROW_API_KEY ?? "";
10
12
 
11
13
  // ─── HTTP helpers ─────────────────────────────────────────────────────────────
14
+ // Authorization header is omitted entirely when key is empty, rather than
15
+ // sending a garbage "Bearer " token — matters for `verify-work`, the one
16
+ // command that works without CASCROW_API_KEY.
17
+
18
+ function authHeader(key) {
19
+ return key ? { "Authorization": `Bearer ${key}` } : {};
20
+ }
12
21
 
13
22
  async function apiPost(path, body, key = API_KEY) {
14
23
  const res = await fetch(`${BASE_URL}${path}`, {
15
24
  method: "POST",
16
25
  headers: {
17
- "Authorization": `Bearer ${key}`,
26
+ ...authHeader(key),
18
27
  "Content-Type": "application/json",
19
28
  },
20
29
  body: JSON.stringify(body),
@@ -26,7 +35,7 @@ async function apiPost(path, body, key = API_KEY) {
26
35
 
27
36
  async function apiGet(path, key = API_KEY) {
28
37
  const res = await fetch(`${BASE_URL}${path}`, {
29
- headers: { "Authorization": `Bearer ${key}` },
38
+ headers: { ...authHeader(key) },
30
39
  });
31
40
  const data = await res.json();
32
41
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
@@ -40,7 +49,7 @@ async function uploadProof(milestoneId, filename, content, key = API_KEY) {
40
49
  form.append("milestoneId", milestoneId);
41
50
  const res = await fetch(`${BASE_URL}/api/proof/upload`, {
42
51
  method: "POST",
43
- headers: { "Authorization": `Bearer ${key}` },
52
+ headers: { ...authHeader(key) },
44
53
  body: form,
45
54
  });
46
55
  const data = await res.json();
@@ -48,16 +57,27 @@ async function uploadProof(milestoneId, filename, content, key = API_KEY) {
48
57
  return data;
49
58
  }
50
59
 
60
+ // Stream verify via SSE — used by the legacy two-step `submit` + `verify --proof <id>` flow
51
61
  async function streamVerify(proofId, key = API_KEY) {
52
- const res = await fetch(`${BASE_URL}/api/verify`, {
53
- method: "POST",
54
- headers: {
55
- "Authorization": `Bearer ${key}`,
56
- "Content-Type": "application/json",
57
- "Accept": "text/event-stream",
58
- },
59
- body: JSON.stringify({ proofId }),
60
- });
62
+ const controller = new AbortController();
63
+ const timeout = setTimeout(() => controller.abort(), 180_000);
64
+
65
+ let res;
66
+ try {
67
+ res = await fetch(`${BASE_URL}/api/verify`, {
68
+ method: "POST",
69
+ headers: {
70
+ ...authHeader(key),
71
+ "Content-Type": "application/json",
72
+ "Accept": "text/event-stream",
73
+ },
74
+ body: JSON.stringify({ proofId }),
75
+ signal: controller.signal,
76
+ });
77
+ } finally {
78
+ clearTimeout(timeout);
79
+ }
80
+
61
81
  if (!res.ok) {
62
82
  const data = await res.json().catch(() => ({}));
63
83
  throw new Error(data.error ?? `HTTP ${res.status}`);
@@ -68,33 +88,38 @@ async function streamVerify(proofId, key = API_KEY) {
68
88
  const decoder = new TextDecoder();
69
89
  let buffer = "";
70
90
 
71
- while (true) {
72
- const { done, value } = await reader.read();
73
- if (done) break;
74
- buffer += decoder.decode(value, { stream: true });
75
- const lines = buffer.split("\n");
76
- buffer = lines.pop() ?? "";
77
- for (const line of lines) {
78
- if (!line.startsWith("data:")) continue;
79
- const raw = line.slice(5).trim();
80
- if (!raw) continue;
81
- try {
82
- const msg = JSON.parse(raw);
83
- if (msg.type === "model_vote") {
84
- votes.push(msg);
85
- const icon = msg.decision === "YES" ? "✅" : "❌";
86
- process.stderr.write(` ${icon} ${msg.model}: ${msg.decision} (${msg.confidence}%)\n`);
87
- } else if (msg.type === "complete") {
88
- return { ...msg, votes };
89
- } else if (msg.type === "error") {
90
- throw new Error(msg.message);
91
+ try {
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+ buffer += decoder.decode(value, { stream: true });
96
+ const lines = buffer.split("\n");
97
+ buffer = lines.pop() ?? "";
98
+ for (const line of lines) {
99
+ if (!line.startsWith("data:")) continue;
100
+ const raw = line.slice(5).trim();
101
+ if (!raw) continue;
102
+ try {
103
+ const msg = JSON.parse(raw);
104
+ if (msg.type === "model_vote") {
105
+ votes.push(msg);
106
+ const icon = msg.decision === "YES" ? "✅" : "❌";
107
+ process.stderr.write(` ${icon} ${msg.model}: ${msg.decision} (${msg.confidence}%)\n`);
108
+ } else if (msg.type === "complete") {
109
+ return { ...msg, votes };
110
+ } else if (msg.type === "error") {
111
+ throw new Error(msg.message);
112
+ }
113
+ } catch (e) {
114
+ if (e instanceof SyntaxError) continue;
115
+ throw e;
91
116
  }
92
- } catch (e) {
93
- if (e instanceof SyntaxError) continue;
94
- throw e;
95
117
  }
96
118
  }
119
+ } finally {
120
+ reader.cancel().catch(() => {});
97
121
  }
122
+
98
123
  throw new Error("Stream ended without a complete event");
99
124
  }
100
125
 
@@ -114,27 +139,108 @@ function usage() {
114
139
  Cascrow CLI — agentic escrow and verification
115
140
 
116
141
  Usage:
117
- cascrow register --email <email> --password <pass> [--name <name>]
118
- cascrow create --title <title> --description <desc> [--days <n>]
119
- cascrow fund --contract <id> [--milestone <id>]
120
- cascrow submit --milestone <id> --proof <text> [--file <name>]
121
- cascrow verify --proof <id>
122
- cascrow get --contract <id>
123
- cascrow join --invite <code>
142
+ cascrow <command> [options]
143
+
144
+ Commands:
145
+ register Create an agent account, get an API key instantly
146
+ verify-work Verify AI-generated work — no contract, no escrow, no account needed
147
+ verify Submit proof and get AI verdict in one call (recommended)
148
+ create Create a milestone contract
149
+ fund Activate a milestone (simulation — no on-chain tx)
150
+ escrow-fund Fund a milestone with real on-chain RLUSD escrow
151
+ submit Upload proof text (returns proofId for legacy verify flow)
152
+ get Get contract status and milestones
153
+ join Join a contract as Builder via invite code
154
+ handoff Send a contract invite to a Builder agent by Agent ID
155
+ check-invites Check pending contract invites for this agent
156
+ me Print this agent's ID
157
+
158
+ Options for each command:
159
+
160
+ register
161
+ --email <email> required
162
+ --password <pass> required (min 8 chars)
163
+ --name <name> optional display name
164
+ --key-name <name> optional label for API key (default: cli)
165
+
166
+ verify-work [NO ACCOUNT NEEDED — 3 free/day per IP]
167
+ --task <description> required what was the AI supposed to do
168
+ --pr <url> GitHub PR URL (github.com only) — or use --code / --image
169
+ --code <text> pasted code, diff, or output (max 50,000 chars)
170
+ --image <path> path to a screenshot/image (.jpg/.png/.webp/.gif, max 8 MB)
171
+
172
+ verify [ONE-SHOT — recommended for code/text proof]
173
+ --contract <id> required contract ID
174
+ --proof <text> required full description of what was completed
175
+ --milestone <id> optional target milestone (defaults to first active)
176
+ --links <url,...> optional comma-separated URLs (live demo, PR, etc.)
177
+ --commit <sha> optional Git commit SHA or PR URL
178
+
179
+ create
180
+ --title <title> required milestone title + acceptance criteria
181
+ --days <n> optional deadline days from now (default: 7)
182
+ --amount <usd> optional RLUSD to lock in escrow (default: 0 = verification-only)
183
+
184
+ fund
185
+ --contract <id> required
186
+ --milestone <id> optional defaults to first available
187
+
188
+ escrow-fund [REAL on-chain RLUSD — agent's own wallet]
189
+ --contract <id> required
190
+ --private-key <key> required EVM private key (funds come from this wallet)
191
+ --amount <usd> optional override milestone amount
192
+ --milestone <id> optional
193
+
194
+ submit [legacy: use verify instead]
195
+ --milestone <id> required
196
+ --proof <text> required
197
+ --file <name> optional filename (default: proof-report.txt)
198
+
199
+ get
200
+ --contract <id> required
201
+
202
+ join
203
+ --invite <code> required invite code from contract
204
+
205
+ handoff
206
+ --contract <id> required
207
+ --invite <code> required invite code from contract
208
+ --to <agentId> required Builder agent's Agent ID
209
+ --message <text> optional instructions for the Builder
210
+
211
+ check-invites no options
212
+
213
+ me no options
124
214
 
125
215
  Environment:
126
- CASCROW_API_KEY Your API key (required for most commands)
216
+ CASCROW_API_KEY Your API key — csk_... (required for every command except verify-work)
127
217
  CASCROW_BASE_URL Override API base (default: https://cascrow.com)
128
218
 
129
219
  Examples:
220
+ # Verify AI-generated work — no account, no escrow, 3 free/day per IP
221
+ cascrow verify-work --task "Fix the 3 security vulnerabilities in the audit report" --pr https://github.com/owner/repo/pull/123
222
+ cascrow verify-work --task "Build a landing page with a working contact form" --image ./screenshot.png
223
+
130
224
  # Register a new agent account (no human in the loop)
131
225
  cascrow register --email bot@example.com --password secret123
132
226
 
133
- # Full verification flow
134
- cascrow create --title "Homepage redesign" --description "Hero section with CTA button, responsive" --days 7
227
+ # One-shot: create contract, fund it, then verify a code fix
228
+ cascrow create --title "Fix auth bug — all tests must pass" --days 7
135
229
  cascrow fund --contract <contractId>
136
- cascrow submit --milestone <milestoneId> --proof "Built hero with Tailwind, deployed at https://..."
137
- cascrow verify --proof <proofId>
230
+ cascrow verify --contract <contractId> --proof "Fixed JWT expiry bug in auth.ts, 42 tests green, PR #51 merged" --commit abc1234 --links https://github.com/you/repo/pull/51
231
+
232
+ # Real escrow flow (RLUSD locked on-chain)
233
+ cascrow create --title "Deploy API" --amount 100 --days 14
234
+ cascrow escrow-fund --contract <contractId> --private-key 0x...
235
+ cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
236
+
237
+ # Agent-to-agent handoff
238
+ cascrow me # → get your Agent ID
239
+ cascrow create --title "Write landing copy" --days 3
240
+ cascrow handoff --contract <id> --invite <code> --to <builderAgentId>
241
+ # (Builder agent runs:)
242
+ cascrow check-invites
243
+ cascrow join --invite <code>
138
244
  `);
139
245
  process.exit(0);
140
246
  }
@@ -178,7 +284,148 @@ async function cmdRegister(args) {
178
284
  const data = await res.json();
179
285
  if (!res.ok) err(data.error ?? `HTTP ${res.status}`);
180
286
  print(data);
181
- console.error("\nSet your API key:\n export CASCROW_API_KEY=" + data.apiKey + "\n");
287
+ console.error("\nSave your API key and set it:\n export CASCROW_API_KEY=" + data.apiKey + "\n");
288
+ }
289
+
290
+ // Verify AI-generated work with no contract, no escrow, no account required.
291
+ // Works without CASCROW_API_KEY (3 free/day per IP, same tier as cascrow.com/verify
292
+ // and the cascrow_verify_work MCP tool) — the one command in this CLI that does.
293
+ async function cmdVerifyWork(args) {
294
+ if (!args.task) err("--task is required");
295
+ if (!args.pr && !args.code && !args.image) err("--pr, --code, or --image is required");
296
+
297
+ // Validate the image up front so errors surface before the "Verifying..." message.
298
+ let imageBuffer, imageMimeType;
299
+ if (args.image) {
300
+ const ext = (args.image.split(".").pop() ?? "").toLowerCase();
301
+ const mimeByExt = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
302
+ imageMimeType = mimeByExt[ext];
303
+ if (!imageMimeType) err("--image must be a .jpg, .jpeg, .png, .webp, or .gif file");
304
+ try {
305
+ imageBuffer = readFileSync(args.image);
306
+ } catch (e) {
307
+ err(`Could not read image file "${args.image}": ${e.message}`);
308
+ }
309
+ }
310
+
311
+ process.stderr.write("\nVerifying with 5 independent AI models (no escrow, no account needed)...\n\n");
312
+
313
+ let data;
314
+ if (args.image) {
315
+ const form = new FormData();
316
+ form.append("taskDescription", args.task);
317
+ form.append("image", new Blob([imageBuffer], { type: imageMimeType }), args.image.split("/").pop());
318
+
319
+ const res = await fetch(`${BASE_URL}/api/verify/standalone`, {
320
+ method: "POST",
321
+ headers: { ...authHeader(API_KEY) },
322
+ body: form,
323
+ });
324
+ data = await res.json();
325
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
326
+ } else {
327
+ const body = { taskDescription: args.task };
328
+ if (args.pr) body.prUrl = args.pr;
329
+ if (args.code) body.codeText = args.code;
330
+ data = await apiPost("/api/verify/standalone", body);
331
+ }
332
+
333
+ const votes = Array.isArray(data.modelVotes) ? data.modelVotes : [];
334
+ for (const v of votes) {
335
+ const icon = v.decision === "YES" ? "✅" : "❌";
336
+ process.stderr.write(` ${icon} ${v.model}: ${v.decision} (${v.confidence}%)\n`);
337
+ }
338
+ process.stderr.write("\n");
339
+
340
+ const statusLine = data.decision === "YES"
341
+ ? `✅ VERIFIED (${data.confidence}% confidence)`
342
+ : `❌ NOT VERIFIED (${data.confidence}% confidence)`;
343
+ process.stderr.write(statusLine + "\n");
344
+ if (data.reportUrl) process.stderr.write(`Full report: ${data.reportUrl}\n\n`);
345
+
346
+ print({
347
+ decision: data.decision,
348
+ confidence: data.confidence,
349
+ reasoning: data.reasoning,
350
+ reportUrl: data.reportUrl,
351
+ publicHash: data.publicHash,
352
+ promptHash: data.promptHash,
353
+ modelVotes: votes,
354
+ });
355
+ }
356
+
357
+ // One-shot verify: submit text proof + get verdict in a single call via /api/mcp/submit.
358
+ // Auto-releases funds and mints NFT on approval — no separate `verify --proof <id>` needed.
359
+ async function cmdVerify(args) {
360
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
361
+ if (!args.contract) err("--contract is required");
362
+ if (!args.proof) err("--proof is required");
363
+
364
+ // Legacy mode: --proof looks like a proof ID (prf_...) and no --contract — use stream verify
365
+ if (!args.contract && args.proof && !args.proof.includes(" ")) {
366
+ process.stderr.write("\nRunning AI verification (5 models)...\n\n");
367
+ const result = await streamVerify(args.proof);
368
+ const passed = result.decision === "YES" && (result.confidence ?? 0) >= 85;
369
+ print({
370
+ decision: result.decision,
371
+ confidence: result.confidence,
372
+ passed,
373
+ action: result.action,
374
+ reasoning: result.reasoning,
375
+ modelVotes: (result.votes ?? []).map(v => ({ model: v.model, decision: v.decision, confidence: v.confidence })),
376
+ });
377
+ return;
378
+ }
379
+
380
+ // One-shot mode: text proof → /api/mcp/submit → verdict + auto-release
381
+ const links = args.links
382
+ ? args.links.split(",").map(l => l.trim()).filter(Boolean)
383
+ : undefined;
384
+
385
+ const evidence = { description: args.proof };
386
+ if (links?.length) evidence.links = links;
387
+ if (args.commit) evidence.github_commit = args.commit;
388
+
389
+ process.stderr.write("\nSubmitting proof and running AI verification (5 models)...\n\n");
390
+
391
+ const data = await apiPost("/api/mcp/submit", {
392
+ contract_id: args.contract,
393
+ milestone_id: args.milestone ?? undefined,
394
+ evidence,
395
+ });
396
+
397
+ const verdict = data.verdict ?? "unknown";
398
+ const confidence = data.confidence ?? 0;
399
+
400
+ // Print model votes if available
401
+ const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
402
+ for (const v of votes) {
403
+ const icon = v.decision === "YES" ? "✅" : "❌";
404
+ process.stderr.write(` ${icon} ${v.model}: ${v.decision} (${v.confidence}%)\n`);
405
+ }
406
+ process.stderr.write("\n");
407
+
408
+ let statusLine;
409
+ if (verdict === "approved") {
410
+ statusLine = `✅ VERIFIED (${confidence}% confidence)`;
411
+ if (data.on_chain_url) statusLine += `\n Proof: ${data.on_chain_url}`;
412
+ } else if (verdict === "pending_review") {
413
+ statusLine = `⏳ PENDING_REVIEW (${confidence}% confidence) — manual review triggered`;
414
+ } else {
415
+ statusLine = `❌ REJECTED (${confidence}% confidence) — resubmit with stronger proof`;
416
+ }
417
+ process.stderr.write(statusLine + "\n\n");
418
+
419
+ print({
420
+ verdict,
421
+ confidence,
422
+ passed: verdict === "approved",
423
+ reasoning: data.reasoning ?? "",
424
+ on_chain_url: data.on_chain_url ?? null,
425
+ proof_id: data.proof_id ?? null,
426
+ signed_at: data.signed_at ?? null,
427
+ model_votes: votes,
428
+ });
182
429
  }
183
430
 
184
431
  async function cmdCreate(args) {
@@ -188,17 +435,19 @@ async function cmdCreate(args) {
188
435
  const deadlineDays = parseInt(args.days ?? "7", 10);
189
436
  const deadline = new Date();
190
437
  deadline.setDate(deadline.getDate() + deadlineDays);
438
+ const amountUSD = parseFloat(args.amount ?? "0");
191
439
 
192
440
  const data = await apiPost("/api/contracts", {
193
441
  milestones: [{
194
- title: args.description ? `${args.title} — ${args.description}` : args.title,
195
- amountUSD: 0,
442
+ title: args.title,
443
+ amountUSD,
196
444
  cancelAfter: deadline.toISOString(),
197
445
  }],
198
446
  });
199
447
 
200
448
  const contractUrl = `${BASE_URL}/contract/${data.contractId}`;
201
449
  print({ ...data, contractUrl });
450
+ process.stderr.write(`\nContract created: ${contractUrl}\n\n`);
202
451
  }
203
452
 
204
453
  async function cmdFund(args) {
@@ -211,8 +460,28 @@ async function cmdFund(args) {
211
460
 
212
461
  const data = await apiPost("/api/agent/fund-milestone", body);
213
462
  print(data);
463
+ process.stderr.write(`\nMilestone funded (simulation). ID: ${data.milestoneId}\n\n`);
214
464
  }
215
465
 
466
+ async function cmdEscrowFund(args) {
467
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
468
+ if (!args.contract) err("--contract is required");
469
+ if (!args["private-key"]) err("--private-key is required (EVM private key, funds come from this wallet)");
470
+
471
+ const body = {
472
+ contractId: args.contract,
473
+ agentPrivateKey: args["private-key"],
474
+ };
475
+ if (args.milestone) body.milestoneId = args.milestone;
476
+ if (args.amount) body.amountUSD = parseFloat(args.amount);
477
+
478
+ process.stderr.write("\nFunding milestone on-chain (approve + fundMilestone)...\n");
479
+ const data = await apiPost("/api/agent/escrow-fund", body);
480
+ print(data);
481
+ process.stderr.write(`\n✅ ${data.amountUSD} RLUSD locked on-chain. TX: ${data.txHash}\n\n`);
482
+ }
483
+
484
+ // Legacy two-step: upload proof → returns proofId → user runs `cascrow verify --proof <id>`
216
485
  async function cmdSubmit(args) {
217
486
  if (!API_KEY) err("CASCROW_API_KEY is not set");
218
487
  if (!args.milestone) err("--milestone is required");
@@ -224,43 +493,57 @@ async function cmdSubmit(args) {
224
493
  args.proof
225
494
  );
226
495
  print(data);
227
- console.error(`\nProof submitted. Run: cascrow verify --proof ${data.proofId}\n`);
496
+ process.stderr.write(`\nProof submitted. Run: cascrow verify --proof ${data.proofId}\n\n`);
228
497
  }
229
498
 
230
- async function cmdVerify(args) {
499
+ async function cmdGet(args) {
231
500
  if (!API_KEY) err("CASCROW_API_KEY is not set");
232
- if (!args.proof) err("--proof is required");
501
+ if (!args.contract) err("--contract is required");
502
+ const data = await apiGet(`/api/contracts/${encodeURIComponent(args.contract)}`);
503
+ print(data);
504
+ }
233
505
 
234
- console.error("\nRunning AI verification (5 models)...\n");
235
- const result = await streamVerify(args.proof);
506
+ async function cmdJoin(args) {
507
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
508
+ if (!args.invite) err("--invite is required");
509
+ const data = await apiPost("/api/contracts/join", { inviteCode: args.invite });
510
+ print(data);
511
+ process.stderr.write(`\nJoined contract: ${data.contractId}\n\n`);
512
+ }
236
513
 
237
- const passed = result.decision === "YES" && result.confidence >= 85;
238
- print({
239
- decision: result.decision,
240
- confidence: result.confidence,
241
- passed,
242
- action: result.action,
243
- reasoning: result.reasoning,
244
- modelVotes: (result.votes ?? []).map(v => ({
245
- model: v.model,
246
- decision: v.decision,
247
- confidence: v.confidence,
248
- })),
514
+ async function cmdHandoff(args) {
515
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
516
+ if (!args.contract) err("--contract is required");
517
+ if (!args.invite) err("--invite is required");
518
+ if (!args.to) err("--to is required (Builder agent's Agent ID)");
519
+
520
+ const data = await apiPost("/api/agent/handoff", {
521
+ contractId: args.contract,
522
+ inviteCode: args.invite,
523
+ builderAgentId: args.to,
524
+ message: args.message ?? undefined,
249
525
  });
526
+ print(data);
527
+ process.stderr.write(`\nContract handed off to agent ${args.to}.\n\n`);
250
528
  }
251
529
 
252
- async function cmdGet(args) {
530
+ async function cmdCheckInvites() {
253
531
  if (!API_KEY) err("CASCROW_API_KEY is not set");
254
- if (!args.contract) err("--contract is required");
255
- const data = await apiGet(`/api/contracts/${args.contract}`);
532
+ const data = await apiGet("/api/agent/pending-invites");
533
+ const invites = data.invites ?? [];
256
534
  print(data);
535
+ if (invites.length === 0) {
536
+ process.stderr.write("\nNo pending invites.\n\n");
537
+ } else {
538
+ process.stderr.write(`\n${invites.length} invite(s) found. Use: cascrow join --invite <code>\n\n`);
539
+ }
257
540
  }
258
541
 
259
- async function cmdJoin(args) {
542
+ async function cmdMe() {
260
543
  if (!API_KEY) err("CASCROW_API_KEY is not set");
261
- if (!args.invite) err("--invite is required");
262
- const data = await apiPost("/api/contracts/join", { inviteCode: args.invite });
544
+ const data = await apiGet("/api/agent/me");
263
545
  print(data);
546
+ process.stderr.write(`\nAgent ID: ${data.agentId}\nShare this with Requester agents for handoff.\n\n`);
264
547
  }
265
548
 
266
549
  // ─── Main ─────────────────────────────────────────────────────────────────────
@@ -274,13 +557,18 @@ if (!command || command === "--help" || command === "help" || command === "-h")
274
557
 
275
558
  try {
276
559
  switch (command) {
277
- case "register": await cmdRegister(args); break;
278
- case "create": await cmdCreate(args); break;
279
- case "fund": await cmdFund(args); break;
280
- case "submit": await cmdSubmit(args); break;
281
- case "verify": await cmdVerify(args); break;
282
- case "get": await cmdGet(args); break;
283
- case "join": await cmdJoin(args); break;
560
+ case "register": await cmdRegister(args); break;
561
+ case "verify-work": await cmdVerifyWork(args); break;
562
+ case "verify": await cmdVerify(args); break;
563
+ case "create": await cmdCreate(args); break;
564
+ case "fund": await cmdFund(args); break;
565
+ case "escrow-fund": await cmdEscrowFund(args); break;
566
+ case "submit": await cmdSubmit(args); break;
567
+ case "get": await cmdGet(args); break;
568
+ case "join": await cmdJoin(args); break;
569
+ case "handoff": await cmdHandoff(args); break;
570
+ case "check-invites": await cmdCheckInvites(); break;
571
+ case "me": await cmdMe(); break;
284
572
  default:
285
573
  console.error(`Unknown command: ${command}\n`);
286
574
  usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cascrow-cli",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for Cascrow — agentic escrow and verification",
5
5
  "type": "module",
6
6
  "main": "index.js",