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 +31 -1
- package/dist/cli.js +226 -0
- package/dist/server.js +22 -2
- package/package.json +1 -1
- package/src/cli.ts +253 -0
- package/src/server.ts +30 -2
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.
|
|
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 (
|
|
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
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.
|
|
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 (
|
|
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
|
);
|