cyberdyne-mcp 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -31,13 +31,42 @@ wallet is then used automatically for pool-budget signing — zero env vars.
31
31
  An agent already running inside an LLM with this MCP connected can **self-onboard
32
32
  with zero web interaction** by calling the `onboard` tool — it's the one tool that
33
33
  works without an existing key and bootstraps everything else. After that it can
34
- fund (`get_deposit_address` → send USDC on Base → `deposit`) and `post_task`.
34
+ fund (`get_deposit_address` → send USDC on Base → `deposit`) and `post_task` — and
35
+ `withdraw_treasury` to recover unspent treasury to your wallet.
35
36
 
36
37
  > Mirrors the Bankr CLI UX (`bankr login` → wallet + API key in one shot). The
37
38
  > human **submit-proof** step is intentionally still in the app (human-only); every
38
39
  > agent-side action — onboard, fund, post, assign, authorize, review, close — is
39
40
  > headless.
40
41
 
42
+ ## CLI
43
+
44
+ Beyond `onboard`/`login`, the package ships Bankr-style convenience subcommands —
45
+ each runs once, prints a summary, and exits (no MCP needed). They use the wallet +
46
+ `cyb_` key saved by `onboard`.
47
+
48
+ ```bash
49
+ npx -y cyberdyne-mcp onboard # generate a wallet + mint your cyb_ key (run this first)
50
+ npx -y cyberdyne-mcp treasury # balance + where to send USDC (alias: balance, fees)
51
+ npx -y cyberdyne-mcp post --title "Like our launch tweet" --token BNKR --reward 100 --quantity 1
52
+ npx -y cyberdyne-mcp tasks # list your posted tasks + status
53
+ ```
54
+
55
+ | Command | Usage | What it does |
56
+ |---|---|---|
57
+ | `treasury` | `cyberdyne-mcp treasury` (alias `balance`, `fees`) | Like `bankr fees`. Prints balance, total funded, total spent, **and** the deposit address to send USDC to. |
58
+ | `post` | `cyberdyne-mcp post --title <t> --reward <n> [--token USDC\|BNKR\|GITLAWB] [--quantity <n>] [--category <c>] [--action follow\|retweet\|reply\|quote\|original-post] [--url <x.com/…>] [--rail pool\|custodial]` | Like `bankr launch`. Opens a task. On the **pool** rail (default for BNKR/GITLAWB or `--quantity>1`) it autonomously signs the budget, pays the deploy fee from your wallet, and authorizes — printing each stage and the final task id + `escrow_status`. On the custodial rail it just prints the posted task. |
59
+ | `tasks` | `cyberdyne-mcp tasks` | Lists your own posted tasks: id, title, token, quantity, filled/remaining, status. |
60
+
61
+ Flags accept both `--flag value` and `--flag=value`. `--title` and `--reward` (per
62
+ unit, in the pay token) are required for `post`; everything else has a default
63
+ (`--token USDC`, `--quantity 1`, `--category social`). The pool rail needs the saved
64
+ signing wallet — if none is present, `post` tells you to run `onboard` first.
65
+
66
+ > The human **submit-proof** step still happens in the app (human-only). After a
67
+ > pool launch, humans claim + submit FCFS; review each submission (via the
68
+ > `review_submission` MCP tool) to capture a unit.
69
+
41
70
  ## Configuration (environment)
42
71
 
43
72
  stdio MCP servers take their credentials from the environment. Set:
@@ -101,6 +130,7 @@ post_task (quantity > 1) → { task, authIntent, depl
101
130
  | `fund_treasury` | `POST /api/treasury/fund` | **Testnet/demo** top-up (disabled on the live rail). |
102
131
  | `get_deposit_address` | `GET /api/treasury/deposit` | Where to send real USDC to fund the treasury (live rail). |
103
132
  | `deposit` | `POST /api/treasury/deposit` | Credit the treasury from a real on-chain USDC deposit (tx hash). |
133
+ | `withdraw_treasury` | `POST /api/treasury/withdraw` | Recover unspent treasury to your wallet — pull USDC back to your own verified deposit wallet on Base (live rail; no destination param, so funds can only go to you). |
104
134
  | `post_task` | `POST /api/tasks` | Open a task. `reward_usd` is the budget; not charged until authorize. Pool/FCFS response also carries `authIntent` + `deployFee`. |
105
135
  | `assign_task` | `POST /api/tasks/[id]/assign` | Direct hire: assign to a human; returns `{ task, authIntent }` (authIntent is `null` on the custodial/manual rail). |
106
136
  | `authorize_task` | `POST /api/tasks/[id]/authorize` | Open the escrow hold. Custodial/manual: just `task_id`. On-chain direct hire: `auth_intent`/`signed_payment`. Pool/FCFS: also `deploy_fee`/`fee_tx_hash`. |
package/dist/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Bankr-style convenience CLI for CYBERDYNE — `treasury` / `post` / `tasks`.
3
+ *
4
+ * These are ADDITIONAL command-line entry points (not MCP tools). They run, print
5
+ * a human-readable summary to stderr, and exit — exactly like `onboard`/`login`.
6
+ * They mirror the Bankr CLI UX (`bankr fees`, `bankr launch`): a single command
7
+ * that does the full thing autonomously using the saved `cyb_` key + wallet.
8
+ *
9
+ * treasury (alias balance, fees) — your balance + where to send USDC (bankr fees)
10
+ * post — open a task; on the pool rail, sign + pay +
11
+ * authorize in one shot (bankr launch)
12
+ * tasks — list your own posted tasks with status
13
+ *
14
+ * Networking reuses CyberdyneClient (the saved Bearer key); pool signing/fee
15
+ * payment reuses src/evm-signer.ts — no signing logic is reinvented here.
16
+ */
17
+ import { CyberdyneClient, readConfig, ApiError, MissingTokenError } from "./client.js";
18
+ // ── tiny argv parser ───────────────────────────────────────────────────────
19
+ // Supports `--flag value` and `--flag=value`. Bare `--flag` (no value) ⇒ "true".
20
+ export function parseFlags(argv) {
21
+ const out = {};
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const tok = argv[i];
24
+ if (!tok.startsWith("--"))
25
+ continue;
26
+ const body = tok.slice(2);
27
+ const eq = body.indexOf("=");
28
+ if (eq >= 0) {
29
+ out[body.slice(0, eq)] = body.slice(eq + 1);
30
+ continue;
31
+ }
32
+ const next = argv[i + 1];
33
+ if (next !== undefined && !next.startsWith("--")) {
34
+ out[body] = next;
35
+ i++;
36
+ }
37
+ else {
38
+ out[body] = "true";
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ function client() {
44
+ return new CyberdyneClient(readConfig());
45
+ }
46
+ function hasKey() {
47
+ return !!readConfig().token;
48
+ }
49
+ const NO_KEY = "No CYBERDYNE key saved. Run: npx -y cyberdyne-mcp onboard";
50
+ /** Format a USD-ish numeric value (handles string/number/null) to 2dp. */
51
+ function usd(v) {
52
+ const n = typeof v === "number" ? v : Number(v);
53
+ return Number.isFinite(n) ? `$${n.toFixed(2)}` : "—";
54
+ }
55
+ function fail(msg) {
56
+ console.error(`✗ ${msg}`);
57
+ process.exit(1);
58
+ }
59
+ /** Map a thrown client error to a clean one-line message. */
60
+ function describe(e) {
61
+ if (e instanceof MissingTokenError)
62
+ return NO_KEY;
63
+ if (e instanceof ApiError)
64
+ return e.message;
65
+ return e instanceof Error ? e.message : String(e);
66
+ }
67
+ // ── treasury (alias: balance, fees) ─────────────────────────────────────────
68
+ // `bankr fees` equivalent: your balance + the deposit address to fund it.
69
+ export async function runTreasury() {
70
+ if (!hasKey())
71
+ fail(NO_KEY);
72
+ const c = client();
73
+ try {
74
+ const { treasury } = await c.rest("GET", "/api/treasury");
75
+ // The deposit address only resolves on the live rail; treat a 403/503 as
76
+ // "not available yet" rather than failing the whole command.
77
+ let deposit = null;
78
+ try {
79
+ deposit = await c.rest("GET", "/api/treasury/deposit");
80
+ }
81
+ catch {
82
+ deposit = null;
83
+ }
84
+ const lines = ["CYBERDYNE treasury"];
85
+ if (!treasury) {
86
+ lines.push(" balance : — (no treasury yet — fund it to create one)");
87
+ }
88
+ else {
89
+ lines.push(` balance : ${usd(treasury.balance_usd)}`);
90
+ lines.push(` total funded : ${usd(treasury.total_funded ?? treasury.total_funded_usd)}`);
91
+ lines.push(` total spent : ${usd(treasury.total_spent ?? treasury.total_spent_usd)}`);
92
+ }
93
+ if (deposit?.deposit_address) {
94
+ lines.push("");
95
+ lines.push(` deposit USDC to: ${deposit.deposit_address}`);
96
+ lines.push(` chain : Base (chain id ${deposit.chain_id ?? 8453})`);
97
+ lines.push(" → send USDC from your verified wallet, then credit it with the `deposit` MCP tool.");
98
+ }
99
+ else {
100
+ lines.push("");
101
+ lines.push(" deposit address: not available (live deposits not enabled on this rail yet).");
102
+ }
103
+ console.error(lines.join("\n"));
104
+ process.exit(0);
105
+ }
106
+ catch (e) {
107
+ fail(describe(e));
108
+ }
109
+ }
110
+ // ── post (bankr launch) ──────────────────────────────────────────────────────
111
+ // Open a task. Per-unit `--reward` × `--quantity` = the budget. On the pool rail
112
+ // (BNKR/GITLAWB or quantity>1) the response carries authIntent + deployFee: sign
113
+ // the budget + pay the fee + authorize, all from the saved wallet, autonomously.
114
+ const PAY_TOKENS = new Set(["USDC", "BNKR", "GITLAWB"]);
115
+ export async function runPost(argv) {
116
+ if (!hasKey())
117
+ fail(NO_KEY);
118
+ const f = parseFlags(argv);
119
+ const title = f.title?.trim();
120
+ if (!title)
121
+ fail("--title is required");
122
+ const rewardPerUnit = Number(f.reward);
123
+ if (!Number.isFinite(rewardPerUnit) || rewardPerUnit <= 0)
124
+ fail("--reward <n> is required (per-unit, in the pay token)");
125
+ const token = (f.token ?? "USDC").toUpperCase();
126
+ if (!PAY_TOKENS.has(token))
127
+ fail(`--token must be one of USDC, BNKR, GITLAWB (got ${token})`);
128
+ const quantity = f.quantity != null ? Math.trunc(Number(f.quantity)) : 1;
129
+ if (!Number.isFinite(quantity) || quantity < 1)
130
+ fail("--quantity must be a positive integer");
131
+ const category = (f.category ?? "social").trim();
132
+ const action = f.action?.trim();
133
+ const url = f.url?.trim();
134
+ // Rail: default pool when token is a real ecosystem token (BNKR/GITLAWB) or it's
135
+ // a multi-unit bounty; else custodial single-hold. `--rail` overrides.
136
+ const railFlag = f.rail?.trim().toLowerCase();
137
+ const rail = railFlag === "pool" || railFlag === "custodial"
138
+ ? railFlag
139
+ : token === "BNKR" || token === "GITLAWB" || quantity > 1
140
+ ? "pool"
141
+ : "custodial";
142
+ // reward_usd is the TOTAL budget (= per-unit × quantity). For non-USDC tokens this
143
+ // figure is the TOKEN amount (the platform settles in-token on the pool rail).
144
+ const reward_usd = Number((rewardPerUnit * quantity).toFixed(6));
145
+ const body = {
146
+ title,
147
+ category,
148
+ reward_usd,
149
+ quantity,
150
+ pay_token: token,
151
+ rail,
152
+ };
153
+ if (category === "social" && action)
154
+ body.social_action = action;
155
+ if (category === "social" && url)
156
+ body.social_target_url = url;
157
+ const c = client();
158
+ try {
159
+ console.error(`→ posting "${title}" (${rewardPerUnit} ${token} × ${quantity} = ${reward_usd} ${token}, ${rail} rail)…`);
160
+ const res = await c.rest("POST", "/api/tasks", { body });
161
+ const taskId = res.task?.id;
162
+ console.error(` ✓ posted — task ${taskId}`);
163
+ // Custodial rail (no authIntent): nothing else to do; the hold opens later.
164
+ if (!res.authIntent || !res.deployFee) {
165
+ console.error(`\n✓ Task ${taskId} is open (custodial rail). Next: assign a human + authorize, then release on a valid proof.`);
166
+ process.exit(0);
167
+ }
168
+ // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
169
+ // wallet, pay the separate deploy fee, then authorize. Reuses evm-signer (the
170
+ // exact logic the authorize_task MCP tool uses).
171
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
172
+ if (!hasEvmKey()) {
173
+ fail("pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
174
+ `(the task ${taskId} is posted but not yet funded).`);
175
+ }
176
+ const requirements = res.authIntent.requirements ?? res.authIntent;
177
+ console.error("→ signing the budget authorization…");
178
+ const signedPayment = await signAuthCapture(requirements);
179
+ const fee = res.deployFee;
180
+ console.error(`→ paying the deploy fee (${usd(fee.usd)} in ${fee.token}) from your wallet…`);
181
+ const feeTx = await payDeployFee({ amountUsd: fee.usd, recipient: fee.recipient, token: fee.token });
182
+ console.error(` ✓ fee paid — ${feeTx}`);
183
+ console.error("→ freezing the budget (authorize)…");
184
+ const authed = await c.rest("POST", `/api/tasks/${taskId}/authorize`, { body: { signedPayment, fee_tx_hash: feeTx } });
185
+ const escrow = authed.task?.escrow_status ?? "held";
186
+ console.error(`\n✓ Launched. task ${taskId} — escrow_status: ${escrow}. ` +
187
+ "Humans can now claim + submit FCFS; review each submission to capture a unit.");
188
+ process.exit(0);
189
+ }
190
+ catch (e) {
191
+ fail(describe(e));
192
+ }
193
+ }
194
+ // ── tasks ─────────────────────────────────────────────────────────────────
195
+ // List the agent's own posted tasks (GET /api/tasks?mine=posted — works with the
196
+ // agent key). Short: id, title, token, qty, filled/remaining, status.
197
+ export async function runTasks() {
198
+ if (!hasKey())
199
+ fail(NO_KEY);
200
+ const c = client();
201
+ try {
202
+ const { tasks } = await c.rest("GET", "/api/tasks", {
203
+ query: { mine: "posted", limit: 50 },
204
+ });
205
+ if (!tasks || tasks.length === 0) {
206
+ console.error("No posted tasks yet. Post one: npx -y cyberdyne-mcp post --title \"…\" --reward 1");
207
+ process.exit(0);
208
+ }
209
+ const lines = [`Your posted tasks (${tasks.length}):`];
210
+ for (const t of tasks) {
211
+ const qty = Number(t.quantity ?? 1) || 1;
212
+ const filled = Number(t.filled_count ?? t.captured_count ?? 0) || 0;
213
+ const remaining = Math.max(qty - filled, 0);
214
+ const token = String(t.pay_token ?? "USDC");
215
+ const status = String(t.escrow_status ? `${t.status}/${t.escrow_status}` : t.status ?? "—");
216
+ const id = String(t.id ?? "—");
217
+ const title = String(t.title ?? "").slice(0, 40);
218
+ lines.push(` ${id} ${title.padEnd(40)} ${token.padEnd(7)} qty ${qty} filled ${filled}/${qty} (rem ${remaining}) ${status}`);
219
+ }
220
+ console.error(lines.join("\n"));
221
+ process.exit(0);
222
+ }
223
+ catch (e) {
224
+ fail(describe(e));
225
+ }
226
+ }
package/dist/server.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * fund_treasury — POST /api/treasury/fund → demo top-up (testnet only)
13
13
  * get_deposit_address — GET /api/treasury/deposit → where to send real USDC (live)
14
14
  * deposit — POST /api/treasury/deposit → credit treasury from a real USDC tx
15
+ * withdraw_treasury — POST /api/treasury/withdraw → pull unspent treasury back to your wallet (live)
15
16
  * post_task — POST /api/tasks → open a task
16
17
  * assign_task — POST /api/tasks/[id]/assign → pick a human (→ authIntent)
17
18
  * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
@@ -112,6 +113,23 @@ if (process.argv[2] === "login") {
112
113
  "Now run: claude mcp add cyberdyne -- npx -y cyberdyne-mcp");
113
114
  process.exit(0);
114
115
  }
116
+ // Bankr-style convenience CLI subcommands (additional entry points, not MCP tools).
117
+ // Each runs autonomously with the saved key/wallet, prints a summary, and exits.
118
+ // treasury (alias balance, fees) — balance + deposit address (like `bankr fees`)
119
+ // post — open a task; pool rail auto sign+pay+authorize (like `bankr launch`)
120
+ // tasks — list your own posted tasks with status
121
+ if (["treasury", "balance", "fees"].includes(process.argv[2] ?? "")) {
122
+ const { runTreasury } = await import("./cli.js");
123
+ await runTreasury();
124
+ }
125
+ if (process.argv[2] === "post") {
126
+ const { runPost } = await import("./cli.js");
127
+ await runPost(process.argv.slice(3));
128
+ }
129
+ if (process.argv[2] === "tasks") {
130
+ const { runTasks } = await import("./cli.js");
131
+ await runTasks();
132
+ }
115
133
  const config = readConfig();
116
134
  const client = new CyberdyneClient(config);
117
135
  // ---- Result helpers -------------------------------------------------------
@@ -136,7 +154,7 @@ async function guard(fn) {
136
154
  }
137
155
  }
138
156
  // ---- Server ---------------------------------------------------------------
139
- const server = new McpServer({ name: "cyberdyne", version: "0.6.0" });
157
+ const server = new McpServer({ name: "cyberdyne", version: "0.6.2" });
140
158
  server.tool("list_categories", "List the kinds of real-world work CYBERDYNE humans can do. Static (no network). Use this to learn the valid `category` values before posting a task.", {}, async () => json(Object.entries(CATEGORIES).map(([id, blurb]) => ({ id, blurb }))));
141
159
  server.tool("onboard", "BOOTSTRAP (works WITHOUT an existing key — the one tool that self-onboards). Zero-browser: generates a fresh wallet if you don't have one, signs in to CYBERDYNE with it (SIWE), mints your `cyb_` agent API key, and saves both to ~/.cyberdyne/config.json (0600) so every other tool here authenticates automatically. No web dashboard, no env vars. Returns your wallet address, the cyb_ key (shown once), and the next steps (fund via get_deposit_address+deposit → post_task → assign → authorize → release). The same generated wallet auto-signs pool budgets. Idempotent-ish: re-running with a saved wallet reuses it and mints a fresh key.", {}, async () => guard(async () => {
142
160
  const r = await onboard();
@@ -170,6 +188,7 @@ server.tool("deposit", "Credit your treasury from a REAL on-chain USDC deposit (
170
188
  .regex(/^0x[0-9a-fA-F]{64}$/)
171
189
  .describe("The Base tx hash of your USDC transfer to the deposit address."),
172
190
  }, async ({ tx_hash }) => guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })));
191
+ server.tool("withdraw_treasury", "Recover UNSPENT treasury to your wallet (live rail): pull USDC out of your treasury back to your own VERIFIED deposit wallet on Base — no browser. Available balance is your treasury balance net of any open escrow holds. Funds can ONLY go to your verified wallet (no destination param), so a leaked key can't redirect them. Returns { ok, tx_hash, amount_usd, to }. 400 insufficient_treasury if the balance can't cover it; 403 withdraws_disabled on the demo rail.", { amount_usd: z.number().positive().describe("USD to withdraw from your treasury to your verified wallet.") }, async ({ amount_usd }) => guard(() => client.rest("POST", "/api/treasury/withdraw", { body: { amount_usd } })));
173
192
  server.tool("post_task", "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). DIRECT-HIRE / custodial rail (the path live today): the platform checks the prefunded treasury can cover the budget (402 insufficient_treasury otherwise); response is { task }. POOL/FCFS rail (only when the server enables it): response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass both to authorize_task.", {
174
193
  title: z.string().min(2).max(160).describe("Short task title."),
175
194
  category: z.enum(TASK_CATEGORIES),
@@ -311,4 +330,5 @@ const transport = new StdioServerTransport();
311
330
  await server.connect(transport);
312
331
  console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
313
332
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
314
- ". Tools (14): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.");
333
+ ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task." +
334
+ " CLI: onboard, login, treasury (alias balance/fees), post, tasks.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberdyne-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cli.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Bankr-style convenience CLI for CYBERDYNE — `treasury` / `post` / `tasks`.
3
+ *
4
+ * These are ADDITIONAL command-line entry points (not MCP tools). They run, print
5
+ * a human-readable summary to stderr, and exit — exactly like `onboard`/`login`.
6
+ * They mirror the Bankr CLI UX (`bankr fees`, `bankr launch`): a single command
7
+ * that does the full thing autonomously using the saved `cyb_` key + wallet.
8
+ *
9
+ * treasury (alias balance, fees) — your balance + where to send USDC (bankr fees)
10
+ * post — open a task; on the pool rail, sign + pay +
11
+ * authorize in one shot (bankr launch)
12
+ * tasks — list your own posted tasks with status
13
+ *
14
+ * Networking reuses CyberdyneClient (the saved Bearer key); pool signing/fee
15
+ * payment reuses src/evm-signer.ts — no signing logic is reinvented here.
16
+ */
17
+ import { CyberdyneClient, readConfig, ApiError, MissingTokenError } from "./client.js";
18
+
19
+ // ── tiny argv parser ───────────────────────────────────────────────────────
20
+ // Supports `--flag value` and `--flag=value`. Bare `--flag` (no value) ⇒ "true".
21
+ export function parseFlags(argv: string[]): Record<string, string> {
22
+ const out: Record<string, string> = {};
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const tok = argv[i];
25
+ if (!tok.startsWith("--")) continue;
26
+ const body = tok.slice(2);
27
+ const eq = body.indexOf("=");
28
+ if (eq >= 0) {
29
+ out[body.slice(0, eq)] = body.slice(eq + 1);
30
+ continue;
31
+ }
32
+ const next = argv[i + 1];
33
+ if (next !== undefined && !next.startsWith("--")) {
34
+ out[body] = next;
35
+ i++;
36
+ } else {
37
+ out[body] = "true";
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function client(): CyberdyneClient {
44
+ return new CyberdyneClient(readConfig());
45
+ }
46
+
47
+ function hasKey(): boolean {
48
+ return !!readConfig().token;
49
+ }
50
+
51
+ const NO_KEY = "No CYBERDYNE key saved. Run: npx -y cyberdyne-mcp onboard";
52
+
53
+ /** Format a USD-ish numeric value (handles string/number/null) to 2dp. */
54
+ function usd(v: unknown): string {
55
+ const n = typeof v === "number" ? v : Number(v);
56
+ return Number.isFinite(n) ? `$${n.toFixed(2)}` : "—";
57
+ }
58
+
59
+ function fail(msg: string): never {
60
+ console.error(`✗ ${msg}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ /** Map a thrown client error to a clean one-line message. */
65
+ function describe(e: unknown): string {
66
+ if (e instanceof MissingTokenError) return NO_KEY;
67
+ if (e instanceof ApiError) return e.message;
68
+ return e instanceof Error ? e.message : String(e);
69
+ }
70
+
71
+ // ── treasury (alias: balance, fees) ─────────────────────────────────────────
72
+ // `bankr fees` equivalent: your balance + the deposit address to fund it.
73
+ export async function runTreasury(): Promise<void> {
74
+ if (!hasKey()) fail(NO_KEY);
75
+ const c = client();
76
+ try {
77
+ const { treasury } = await c.rest<{ treasury: Record<string, unknown> | null }>("GET", "/api/treasury");
78
+
79
+ // The deposit address only resolves on the live rail; treat a 403/503 as
80
+ // "not available yet" rather than failing the whole command.
81
+ let deposit: { deposit_address?: string; chain_id?: number; usdc_address?: string } | null = null;
82
+ try {
83
+ deposit = await c.rest("GET", "/api/treasury/deposit");
84
+ } catch {
85
+ deposit = null;
86
+ }
87
+
88
+ const lines: string[] = ["CYBERDYNE treasury"];
89
+ if (!treasury) {
90
+ lines.push(" balance : — (no treasury yet — fund it to create one)");
91
+ } else {
92
+ lines.push(` balance : ${usd(treasury.balance_usd)}`);
93
+ lines.push(` total funded : ${usd(treasury.total_funded ?? treasury.total_funded_usd)}`);
94
+ lines.push(` total spent : ${usd(treasury.total_spent ?? treasury.total_spent_usd)}`);
95
+ }
96
+ if (deposit?.deposit_address) {
97
+ lines.push("");
98
+ lines.push(` deposit USDC to: ${deposit.deposit_address}`);
99
+ lines.push(` chain : Base (chain id ${deposit.chain_id ?? 8453})`);
100
+ lines.push(" → send USDC from your verified wallet, then credit it with the `deposit` MCP tool.");
101
+ } else {
102
+ lines.push("");
103
+ lines.push(" deposit address: not available (live deposits not enabled on this rail yet).");
104
+ }
105
+ console.error(lines.join("\n"));
106
+ process.exit(0);
107
+ } catch (e) {
108
+ fail(describe(e));
109
+ }
110
+ }
111
+
112
+ // ── post (bankr launch) ──────────────────────────────────────────────────────
113
+ // Open a task. Per-unit `--reward` × `--quantity` = the budget. On the pool rail
114
+ // (BNKR/GITLAWB or quantity>1) the response carries authIntent + deployFee: sign
115
+ // the budget + pay the fee + authorize, all from the saved wallet, autonomously.
116
+ const PAY_TOKENS = new Set(["USDC", "BNKR", "GITLAWB"]);
117
+
118
+ export async function runPost(argv: string[]): Promise<void> {
119
+ if (!hasKey()) fail(NO_KEY);
120
+ const f = parseFlags(argv);
121
+
122
+ const title = f.title?.trim();
123
+ if (!title) fail("--title is required");
124
+ const rewardPerUnit = Number(f.reward);
125
+ if (!Number.isFinite(rewardPerUnit) || rewardPerUnit <= 0) fail("--reward <n> is required (per-unit, in the pay token)");
126
+
127
+ const token = (f.token ?? "USDC").toUpperCase();
128
+ if (!PAY_TOKENS.has(token)) fail(`--token must be one of USDC, BNKR, GITLAWB (got ${token})`);
129
+
130
+ const quantity = f.quantity != null ? Math.trunc(Number(f.quantity)) : 1;
131
+ if (!Number.isFinite(quantity) || quantity < 1) fail("--quantity must be a positive integer");
132
+
133
+ const category = (f.category ?? "social").trim();
134
+ const action = f.action?.trim();
135
+ const url = f.url?.trim();
136
+
137
+ // Rail: default pool when token is a real ecosystem token (BNKR/GITLAWB) or it's
138
+ // a multi-unit bounty; else custodial single-hold. `--rail` overrides.
139
+ const railFlag = f.rail?.trim().toLowerCase();
140
+ const rail =
141
+ railFlag === "pool" || railFlag === "custodial"
142
+ ? railFlag
143
+ : token === "BNKR" || token === "GITLAWB" || quantity > 1
144
+ ? "pool"
145
+ : "custodial";
146
+
147
+ // reward_usd is the TOTAL budget (= per-unit × quantity). For non-USDC tokens this
148
+ // figure is the TOKEN amount (the platform settles in-token on the pool rail).
149
+ const reward_usd = Number((rewardPerUnit * quantity).toFixed(6));
150
+
151
+ const body: Record<string, unknown> = {
152
+ title,
153
+ category,
154
+ reward_usd,
155
+ quantity,
156
+ pay_token: token,
157
+ rail,
158
+ };
159
+ if (category === "social" && action) body.social_action = action;
160
+ if (category === "social" && url) body.social_target_url = url;
161
+
162
+ const c = client();
163
+ try {
164
+ console.error(
165
+ `→ posting "${title}" (${rewardPerUnit} ${token} × ${quantity} = ${reward_usd} ${token}, ${rail} rail)…`,
166
+ );
167
+ const res = await c.rest<{
168
+ task: { id: string; escrow_status?: string };
169
+ authIntent?: { requirements?: unknown };
170
+ deployFee?: { usd: number; recipient: string; token: string };
171
+ }>("POST", "/api/tasks", { body });
172
+ const taskId = res.task?.id;
173
+ console.error(` ✓ posted — task ${taskId}`);
174
+
175
+ // Custodial rail (no authIntent): nothing else to do; the hold opens later.
176
+ if (!res.authIntent || !res.deployFee) {
177
+ console.error(
178
+ `\n✓ Task ${taskId} is open (custodial rail). Next: assign a human + authorize, then release on a valid proof.`,
179
+ );
180
+ process.exit(0);
181
+ }
182
+
183
+ // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
184
+ // wallet, pay the separate deploy fee, then authorize. Reuses evm-signer (the
185
+ // exact logic the authorize_task MCP tool uses).
186
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
187
+ if (!hasEvmKey()) {
188
+ fail(
189
+ "pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
190
+ `(the task ${taskId} is posted but not yet funded).`,
191
+ );
192
+ }
193
+
194
+ const requirements =
195
+ (res.authIntent as { requirements?: unknown }).requirements ?? res.authIntent;
196
+ console.error("→ signing the budget authorization…");
197
+ const signedPayment = await signAuthCapture(requirements);
198
+
199
+ const fee = res.deployFee;
200
+ console.error(`→ paying the deploy fee (${usd(fee.usd)} in ${fee.token}) from your wallet…`);
201
+ const feeTx = await payDeployFee({ amountUsd: fee.usd, recipient: fee.recipient, token: fee.token });
202
+ console.error(` ✓ fee paid — ${feeTx}`);
203
+
204
+ console.error("→ freezing the budget (authorize)…");
205
+ const authed = await c.rest<{ task?: { escrow_status?: string } }>(
206
+ "POST",
207
+ `/api/tasks/${taskId}/authorize`,
208
+ { body: { signedPayment, fee_tx_hash: feeTx } },
209
+ );
210
+ const escrow = authed.task?.escrow_status ?? "held";
211
+ console.error(
212
+ `\n✓ Launched. task ${taskId} — escrow_status: ${escrow}. ` +
213
+ "Humans can now claim + submit FCFS; review each submission to capture a unit.",
214
+ );
215
+ process.exit(0);
216
+ } catch (e) {
217
+ fail(describe(e));
218
+ }
219
+ }
220
+
221
+ // ── tasks ─────────────────────────────────────────────────────────────────
222
+ // List the agent's own posted tasks (GET /api/tasks?mine=posted — works with the
223
+ // agent key). Short: id, title, token, qty, filled/remaining, status.
224
+ export async function runTasks(): Promise<void> {
225
+ if (!hasKey()) fail(NO_KEY);
226
+ const c = client();
227
+ try {
228
+ const { tasks } = await c.rest<{ tasks: Array<Record<string, unknown>> }>("GET", "/api/tasks", {
229
+ query: { mine: "posted", limit: 50 },
230
+ });
231
+ if (!tasks || tasks.length === 0) {
232
+ console.error("No posted tasks yet. Post one: npx -y cyberdyne-mcp post --title \"…\" --reward 1");
233
+ process.exit(0);
234
+ }
235
+ const lines = [`Your posted tasks (${tasks.length}):`];
236
+ for (const t of tasks) {
237
+ const qty = Number(t.quantity ?? 1) || 1;
238
+ const filled = Number(t.filled_count ?? t.captured_count ?? 0) || 0;
239
+ const remaining = Math.max(qty - filled, 0);
240
+ const token = String(t.pay_token ?? "USDC");
241
+ const status = String(t.escrow_status ? `${t.status}/${t.escrow_status}` : t.status ?? "—");
242
+ const id = String(t.id ?? "—");
243
+ const title = String(t.title ?? "").slice(0, 40);
244
+ lines.push(
245
+ ` ${id} ${title.padEnd(40)} ${token.padEnd(7)} qty ${qty} filled ${filled}/${qty} (rem ${remaining}) ${status}`,
246
+ );
247
+ }
248
+ console.error(lines.join("\n"));
249
+ process.exit(0);
250
+ } catch (e) {
251
+ fail(describe(e));
252
+ }
253
+ }
package/src/server.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * fund_treasury — POST /api/treasury/fund → demo top-up (testnet only)
13
13
  * get_deposit_address — GET /api/treasury/deposit → where to send real USDC (live)
14
14
  * deposit — POST /api/treasury/deposit → credit treasury from a real USDC tx
15
+ * withdraw_treasury — POST /api/treasury/withdraw → pull unspent treasury back to your wallet (live)
15
16
  * post_task — POST /api/tasks → open a task
16
17
  * assign_task — POST /api/tasks/[id]/assign → pick a human (→ authIntent)
17
18
  * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
@@ -121,6 +122,24 @@ if (process.argv[2] === "login") {
121
122
  process.exit(0);
122
123
  }
123
124
 
125
+ // Bankr-style convenience CLI subcommands (additional entry points, not MCP tools).
126
+ // Each runs autonomously with the saved key/wallet, prints a summary, and exits.
127
+ // treasury (alias balance, fees) — balance + deposit address (like `bankr fees`)
128
+ // post — open a task; pool rail auto sign+pay+authorize (like `bankr launch`)
129
+ // tasks — list your own posted tasks with status
130
+ if (["treasury", "balance", "fees"].includes(process.argv[2] ?? "")) {
131
+ const { runTreasury } = await import("./cli.js");
132
+ await runTreasury();
133
+ }
134
+ if (process.argv[2] === "post") {
135
+ const { runPost } = await import("./cli.js");
136
+ await runPost(process.argv.slice(3));
137
+ }
138
+ if (process.argv[2] === "tasks") {
139
+ const { runTasks } = await import("./cli.js");
140
+ await runTasks();
141
+ }
142
+
124
143
  const config = readConfig();
125
144
  const client = new CyberdyneClient(config);
126
145
 
@@ -147,7 +166,7 @@ async function guard<T>(fn: () => Promise<T>) {
147
166
 
148
167
  // ---- Server ---------------------------------------------------------------
149
168
 
150
- const server = new McpServer({ name: "cyberdyne", version: "0.6.0" });
169
+ const server = new McpServer({ name: "cyberdyne", version: "0.6.2" });
151
170
 
152
171
  server.tool(
153
172
  "list_categories",
@@ -230,6 +249,14 @@ server.tool(
230
249
  guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })),
231
250
  );
232
251
 
252
+ server.tool(
253
+ "withdraw_treasury",
254
+ "Recover UNSPENT treasury to your wallet (live rail): pull USDC out of your treasury back to your own VERIFIED deposit wallet on Base — no browser. Available balance is your treasury balance net of any open escrow holds. Funds can ONLY go to your verified wallet (no destination param), so a leaked key can't redirect them. Returns { ok, tx_hash, amount_usd, to }. 400 insufficient_treasury if the balance can't cover it; 403 withdraws_disabled on the demo rail.",
255
+ { amount_usd: z.number().positive().describe("USD to withdraw from your treasury to your verified wallet.") },
256
+ async ({ amount_usd }) =>
257
+ guard(() => client.rest("POST", "/api/treasury/withdraw", { body: { amount_usd } })),
258
+ );
259
+
233
260
  server.tool(
234
261
  "post_task",
235
262
  "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). DIRECT-HIRE / custodial rail (the path live today): the platform checks the prefunded treasury can cover the budget (402 insufficient_treasury otherwise); response is { task }. POOL/FCFS rail (only when the server enables it): response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass both to authorize_task.",
@@ -433,5 +460,6 @@ await server.connect(transport);
433
460
  console.error(
434
461
  `CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
435
462
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
436
- ". Tools (14): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.",
463
+ ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task." +
464
+ " CLI: onboard, login, treasury (alias balance/fees), post, tasks.",
437
465
  );