viveworker 0.8.1 → 0.8.3
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 +64 -4
- package/package.json +1 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +1 -1
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/scripts/a2a-executor.mjs +261 -7
- package/scripts/a2a-handler.mjs +88 -0
- package/scripts/a2a-relay-client.mjs +6 -0
- package/scripts/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/viveworker-bridge.mjs +1909 -183
- package/web/app.css +147 -1
- package/web/app.js +1205 -131
- package/web/build-id.js +1 -1
- package/web/i18n.js +147 -17
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +250 -13
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +14 -4
package/README.md
CHANGED
|
@@ -253,6 +253,8 @@ enabled = true
|
|
|
253
253
|
|
|
254
254
|
Restart Codex after changing the config. For normal MCP usage, `npx viveworker enable mcp --target codex` is still the recommended path.
|
|
255
255
|
|
|
256
|
+
For plugin release readiness and submission notes, see [`plugins/viveworker-control-plane/DISTRIBUTION.md`](plugins/viveworker-control-plane/DISTRIBUTION.md).
|
|
257
|
+
|
|
256
258
|
## File Share
|
|
257
259
|
|
|
258
260
|
`viveworker` includes **File Share**, a private file-hosting surface for agent outputs. It is useful when an agent generates a report, PDF, screenshot, interactive prototype, or CSV and should hand back a URL instead of pasting a blob into chat.
|
|
@@ -384,18 +386,18 @@ Because the Claude hook opens browser windows and returns focus to Claude Deskto
|
|
|
384
386
|
|
|
385
387
|
### What it does
|
|
386
388
|
|
|
387
|
-
- **Incoming
|
|
389
|
+
- **Incoming reply drafts**: detects when other agents comment on your posts, drafts a contextual reply first, and sends it to your phone for approval
|
|
388
390
|
- **Draft approval on phone**: reply drafts and original post drafts appear in `Tasks` and `Timeline`, where you can approve, deny, or edit them from your phone
|
|
389
|
-
- **Auto-scout replies**: every 2 minutes, scans the Moltbook feed, scores posts against your agent's persona (0–100), batches candidates over a 30-minute window, picks the best match, drafts a reply via LLM, and proposes it for your approval
|
|
391
|
+
- **Auto-scout replies**: every 2 minutes, handles pending incoming comments first, then scans the Moltbook feed, scores posts against your agent's persona (0–100), batches candidates over a 30-minute window, picks the best match, drafts a reply via LLM, and proposes it for your approval
|
|
390
392
|
- **Original post drafts**: based on your daily coding activity, composes new posts in your agent's voice and proposes them at natural intervals — morning (yesterday recap), noon (morning progress), and evening (full-day summary). Up to 3 per day; deny any slot you don't want
|
|
391
393
|
|
|
392
394
|
### How it works
|
|
393
395
|
|
|
394
396
|
1. Define your agent's persona in `~/.viveworker/moltbook-persona.md` — voice, expertise, interests, topics to avoid
|
|
395
397
|
2. The system filters all content through this persona: only activities and posts that match your agent's expertise are surfaced
|
|
396
|
-
3. The Moltbook watcher
|
|
398
|
+
3. The Moltbook watcher saves incoming replies; auto-scout turns pending comments and discovery candidates into draft proposals
|
|
397
399
|
4. On your phone, you can approve, deny, or edit the draft body before sending
|
|
398
|
-
5.
|
|
400
|
+
5. After approval, the bridge posts to Moltbook and solves the verification puzzle automatically
|
|
399
401
|
|
|
400
402
|
### Setup
|
|
401
403
|
|
|
@@ -426,6 +428,7 @@ Open `Settings > Moltbook` in the phone app to see the current auto-scout postin
|
|
|
426
428
|
- `npx viveworker moltbook list` — show pending comment notifications
|
|
427
429
|
- `npx viveworker moltbook poll` — manually refresh Moltbook notifications once
|
|
428
430
|
- `npx viveworker moltbook reconcile` — resolve inbox items that were already replied to elsewhere
|
|
431
|
+
- `npx viveworker moltbook inbox-pick` — manually pick one pending incoming comment for a reply draft
|
|
429
432
|
- `npx viveworker moltbook scout` — manually pick a feed candidate
|
|
430
433
|
- `npx viveworker moltbook propose <postId> --text "..."` — submit a reply draft for phone approval
|
|
431
434
|
- `npx viveworker moltbook compose` — inspect today's activity for original-post material
|
|
@@ -463,6 +466,63 @@ npx viveworker enable a2a --user-id <desired-id> \
|
|
|
463
466
|
|
|
464
467
|
The bridge detects the new credentials within 30 seconds and auto-connects.
|
|
465
468
|
|
|
469
|
+
### A2A Pro + x402 test flow
|
|
470
|
+
|
|
471
|
+
Experimental paid deliverables can be tested by adding viveworker metadata to an incoming A2A task.
|
|
472
|
+
The task still requires phone approval, executes locally, uploads the result to File Share with an x402 payment gate, and returns only the unlock URL to the requester.
|
|
473
|
+
|
|
474
|
+
On the receiving side, set defaults if you do not want every caller to provide payment metadata:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
A2A_PRO_MODEL=gpt-5.5
|
|
478
|
+
A2A_PRO_PRICE=1.00
|
|
479
|
+
A2A_PRO_PAY_TO=0x1111111111111111111111111111111111111111
|
|
480
|
+
A2A_PRO_EXPIRES_DAYS=7
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Only set `A2A_PRO_MODEL` / `requestedModel` to a model name supported by the local Codex or Claude CLI you will use for execution.
|
|
484
|
+
|
|
485
|
+
Example requester payload:
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"jsonrpc": "2.0",
|
|
490
|
+
"id": "research-1",
|
|
491
|
+
"method": "message/send",
|
|
492
|
+
"params": {
|
|
493
|
+
"message": {
|
|
494
|
+
"role": "user",
|
|
495
|
+
"parts": [
|
|
496
|
+
{
|
|
497
|
+
"type": "text",
|
|
498
|
+
"text": "Research the current state of agent-to-agent paid deliverables and return a concise brief."
|
|
499
|
+
}
|
|
500
|
+
],
|
|
501
|
+
"metadata": {
|
|
502
|
+
"viveworker": {
|
|
503
|
+
"mode": "x402-pro",
|
|
504
|
+
"requestedTier": "pro",
|
|
505
|
+
"requestedModel": "gpt-5.5",
|
|
506
|
+
"deliverableType": "research brief",
|
|
507
|
+
"payment": {
|
|
508
|
+
"price": "1.00",
|
|
509
|
+
"payTo": "0x1111111111111111111111111111111111111111"
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
When completed, `tasks/get` returns an artifact that contains the File Share unlock URL. The requester can inspect the x402 requirement with:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
npx viveworker share pay <unlock-url> --dry-run
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Then they can unlock with their configured buyer wallet, for example `VIVEWORKER_BUYER_PRIVATE_KEY=0x... npx viveworker share pay <unlock-url> --output ./deliverable.html`.
|
|
525
|
+
|
|
466
526
|
### Key commands
|
|
467
527
|
|
|
468
528
|
- `npx viveworker enable a2a --user-id <id>` — register with the relay via GitHub OAuth
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Open mobile control surface for Codex, Claude, Thread Sharing, File Share, Moltbook, and A2A tasks on your trusted LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Codex Plugin Distribution Checklist
|
|
2
|
+
|
|
3
|
+
This checklist tracks the release readiness for the `viveworker-control-plane` Codex plugin.
|
|
4
|
+
|
|
5
|
+
## Plugin package
|
|
6
|
+
|
|
7
|
+
- Plugin name: `viveworker-control-plane`
|
|
8
|
+
- Display name: `viveworker`
|
|
9
|
+
- Version: `0.8.1`
|
|
10
|
+
- Category: `Productivity`
|
|
11
|
+
- Developer name: `viveworker team`
|
|
12
|
+
- Contact: `hello@viveworker.com`
|
|
13
|
+
- Website: `https://viveworker.com/`
|
|
14
|
+
- Repository: `https://github.com/viveworker-dev/viveworker`
|
|
15
|
+
- License: `MIT`
|
|
16
|
+
- Plugin manifest: `plugins/viveworker-control-plane/.codex-plugin/plugin.json`
|
|
17
|
+
- Marketplace entry: `.agents/plugins/marketplace.json`
|
|
18
|
+
|
|
19
|
+
## User-facing positioning
|
|
20
|
+
|
|
21
|
+
Short description:
|
|
22
|
+
|
|
23
|
+
> Control Codex, Claude, and agent tasks from your phone.
|
|
24
|
+
|
|
25
|
+
Long description:
|
|
26
|
+
|
|
27
|
+
> Pair your phone with viveworker to control Codex, Claude, File Share, Thread Share, A2A tasks, and social posting workflows like Moltbook. Approve actions, answer questions, receive updates, share deliverables, delegate work, and keep agent work moving without being tied to your computer.
|
|
28
|
+
|
|
29
|
+
## Included capabilities
|
|
30
|
+
|
|
31
|
+
- MCP stdio server config via `plugins/viveworker-control-plane/.mcp.json`
|
|
32
|
+
- Codex skill guidance via `plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md`
|
|
33
|
+
- Plugin logo and composer icon via `plugins/viveworker-control-plane/assets/viveworker-logo-v2.png`
|
|
34
|
+
- First-run guidance for setup, pairing, MCP status checks, and smoke notifications
|
|
35
|
+
- Tool policy for ask, approval, notify, File Share, Thread Share, and A2A delegation
|
|
36
|
+
|
|
37
|
+
## Security posture
|
|
38
|
+
|
|
39
|
+
- MCP is stdio-only.
|
|
40
|
+
- No HTTP MCP server is exposed by the plugin.
|
|
41
|
+
- The plugin does not execute arbitrary shell commands.
|
|
42
|
+
- File Share requires phone approval and is restricted by viveworker's path safety rules.
|
|
43
|
+
- A2A task sending uses registered target aliases and requires phone approval.
|
|
44
|
+
- Secrets, credentials, private keys, relay tokens, public keys, file contents, and command text must not be sent through plugin guidance or central analytics.
|
|
45
|
+
|
|
46
|
+
## Local validation
|
|
47
|
+
|
|
48
|
+
Run these before proposing a plugin release:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node -e "JSON.parse(require('fs').readFileSync('plugins/viveworker-control-plane/.codex-plugin/plugin.json','utf8')); JSON.parse(require('fs').readFileSync('.agents/plugins/marketplace.json','utf8')); JSON.parse(require('fs').readFileSync('plugins/viveworker-control-plane/.mcp.json','utf8')); console.log('ok')"
|
|
52
|
+
npm pack --dry-run
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Manual smoke:
|
|
56
|
+
|
|
57
|
+
1. Add the local marketplace to Codex.
|
|
58
|
+
2. Enable `viveworker-control-plane@viveworker`.
|
|
59
|
+
3. Restart Codex.
|
|
60
|
+
4. Confirm the plugin is visible.
|
|
61
|
+
5. Call `viveworker_status`.
|
|
62
|
+
6. Send a `viveworker_notify` smoke message.
|
|
63
|
+
7. Confirm the phone receives the notification and the timeline records `provider: "mcp"`.
|
|
64
|
+
|
|
65
|
+
## Codex local marketplace config
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[marketplaces.viveworker]
|
|
69
|
+
source_type = "local"
|
|
70
|
+
source = "/path/to/viveworker"
|
|
71
|
+
|
|
72
|
+
[plugins."viveworker-control-plane@viveworker"]
|
|
73
|
+
enabled = true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Public distribution readiness
|
|
77
|
+
|
|
78
|
+
Ready:
|
|
79
|
+
|
|
80
|
+
- Plugin manifest exists and has production metadata.
|
|
81
|
+
- Marketplace entry exists for local testing.
|
|
82
|
+
- MCP config uses `npx viveworker mcp`.
|
|
83
|
+
- Skill guidance is bundled.
|
|
84
|
+
- Logo assets are bundled.
|
|
85
|
+
- npm package includes the plugin directory.
|
|
86
|
+
|
|
87
|
+
Open items before formal marketplace submission:
|
|
88
|
+
|
|
89
|
+
- Confirm the current Codex plugin marketplace submission channel and required review format.
|
|
90
|
+
- Confirm whether marketplace reviewers require screenshots, a privacy summary, or a demo video.
|
|
91
|
+
- Confirm whether plugin version should track npm version for every patch release.
|
|
92
|
+
- Confirm whether `policy.authentication` should remain `ON_INSTALL` or move to `ON_USE` if the marketplace review prefers deferred setup.
|
|
93
|
+
|
|
94
|
+
## Suggested submission blurb
|
|
95
|
+
|
|
96
|
+
viveworker is an AI agent lifecycle control plane that lets developers run Codex, Claude, File Share, Thread Share, A2A delegation, and Moltbook workflows from a paired phone. The Codex plugin bundles MCP configuration and agent guidance so Codex can ask questions, request approvals, send mobile notifications, share deliverables, hand off context, and delegate tasks through viveworker without requiring users to manually wire the workflow together.
|
package/scripts/a2a-executor.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import os from "node:os";
|
|
13
|
+
import { Blob, File } from "node:buffer";
|
|
13
14
|
import { completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
14
15
|
|
|
15
16
|
const APP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
|
|
@@ -176,13 +177,39 @@ export async function executeA2ATask(task, config, runtime, state, { recordTimel
|
|
|
176
177
|
executor = available.codex ? "codex" : available.claude ? "claude" : "codex";
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
const executionOptions = resolveA2AExecutionOptions(task, config, executor);
|
|
181
|
+
const executionInstruction = buildA2AExecutionInstruction(instruction, task);
|
|
182
|
+
|
|
183
|
+
console.log(
|
|
184
|
+
`[a2a-exec] Starting task ${task.id} via ${executor}${executionOptions.model ? ` model=${executionOptions.model}` : ""}: ${instruction.slice(0, 80)}`
|
|
185
|
+
);
|
|
180
186
|
|
|
181
187
|
try {
|
|
182
188
|
const result = executor === "claude"
|
|
183
|
-
? await runClaudeExec(
|
|
184
|
-
: await runCodexExec(
|
|
185
|
-
|
|
189
|
+
? await runClaudeExec(executionInstruction, executionOptions)
|
|
190
|
+
: await runCodexExec(executionInstruction, config, executionOptions);
|
|
191
|
+
|
|
192
|
+
if (shouldUploadPaidDeliverable(task)) {
|
|
193
|
+
const paidShare = await uploadPaidA2ADeliverable({ config, task, resultText: result });
|
|
194
|
+
const responseText = buildPaidUnlockResponse({ task, paidShare });
|
|
195
|
+
completeA2ATask(task, responseText);
|
|
196
|
+
task.paidDeliverable = paidShare;
|
|
197
|
+
if (task.artifacts?.[0]) {
|
|
198
|
+
task.artifacts[0].name = "x402 unlockable deliverable";
|
|
199
|
+
task.artifacts[0].metadata = {
|
|
200
|
+
viveworker: {
|
|
201
|
+
mode: "x402-pro",
|
|
202
|
+
url: paidShare.url,
|
|
203
|
+
slug: paidShare.slug,
|
|
204
|
+
price: paidShare.price,
|
|
205
|
+
payTo: paidShare.payTo,
|
|
206
|
+
network: paidShare.network,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
completeA2ATask(task, result);
|
|
212
|
+
}
|
|
186
213
|
console.log(`[a2a-exec] Task ${task.id} completed via ${executor} (${result.length} chars)`);
|
|
187
214
|
} catch (error) {
|
|
188
215
|
failA2ATask(task, error.message);
|
|
@@ -206,6 +233,8 @@ export async function executeA2ATask(task, config, runtime, state, { recordTimel
|
|
|
206
233
|
messageText: task.status === "completed"
|
|
207
234
|
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 500)
|
|
208
235
|
: task.statusMessage || "Failed",
|
|
236
|
+
viveworker: task.viveworker || {},
|
|
237
|
+
paidDeliverable: task.paidDeliverable || null,
|
|
209
238
|
taskStatus: task.status,
|
|
210
239
|
createdAtMs: task.updatedAtMs || Date.now(),
|
|
211
240
|
readOnly: true,
|
|
@@ -237,6 +266,16 @@ export async function executeA2ATask(task, config, runtime, state, { recordTimel
|
|
|
237
266
|
stableId: `a2a_task_result:${task.id}`,
|
|
238
267
|
title: `A2A ${icon}: ${instruction.slice(0, 60)}`,
|
|
239
268
|
body: resultBody || instruction.slice(0, 160),
|
|
269
|
+
buildLocalizedContent: ({ locale }) => {
|
|
270
|
+
const lang = locale?.startsWith("ja") ? "ja" : "en";
|
|
271
|
+
const instructionSnippet = instruction.slice(0, 60);
|
|
272
|
+
return {
|
|
273
|
+
title: isCompleted
|
|
274
|
+
? (lang === "ja" ? `A2A 完了: ${instructionSnippet}` : `A2A completed: ${instructionSnippet}`)
|
|
275
|
+
: (lang === "ja" ? `A2A 失敗: ${instructionSnippet}` : `A2A failed: ${instructionSnippet}`),
|
|
276
|
+
body: resultBody || instruction.slice(0, 160),
|
|
277
|
+
};
|
|
278
|
+
},
|
|
240
279
|
});
|
|
241
280
|
} catch (error) {
|
|
242
281
|
console.error(`[a2a-exec-push] ${error.message}`);
|
|
@@ -244,13 +283,225 @@ export async function executeA2ATask(task, config, runtime, state, { recordTimel
|
|
|
244
283
|
}
|
|
245
284
|
}
|
|
246
285
|
|
|
286
|
+
export function resolveA2AExecutionOptions(task, config, executor) {
|
|
287
|
+
const spec = task?.viveworker || {};
|
|
288
|
+
const requestedModel = cleanText(spec.requestedModel || "");
|
|
289
|
+
const tier = cleanText(spec.requestedTier || "");
|
|
290
|
+
const proModel = cleanText(config?.a2aProModel || process.env.A2A_PRO_MODEL || "");
|
|
291
|
+
return {
|
|
292
|
+
model: requestedModel || (tier === "pro" ? proModel : ""),
|
|
293
|
+
tier,
|
|
294
|
+
executor,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function buildA2AExecutionInstruction(instruction, task) {
|
|
299
|
+
const spec = task?.viveworker || {};
|
|
300
|
+
const tier = cleanText(spec.requestedTier || "");
|
|
301
|
+
if (tier !== "pro") {
|
|
302
|
+
return instruction;
|
|
303
|
+
}
|
|
304
|
+
const deliverableType = cleanText(spec.deliverableType || "research brief");
|
|
305
|
+
return [
|
|
306
|
+
"You are completing a paid A2A Pro deliverable.",
|
|
307
|
+
`Deliverable type: ${deliverableType}.`,
|
|
308
|
+
"Prioritize accuracy, clear structure, and actionable conclusions. Do not mention internal account tiers or subscription access.",
|
|
309
|
+
"",
|
|
310
|
+
instruction,
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function shouldUploadPaidDeliverable(task) {
|
|
315
|
+
return task?.viveworker?.paidDeliverable === true || cleanText(task?.viveworker?.mode || "") === "x402-pro";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolvePaidShareSpec(config, task) {
|
|
319
|
+
const spec = task?.viveworker || {};
|
|
320
|
+
const payment = spec.payment || {};
|
|
321
|
+
const price = cleanText(payment.price || config?.a2aProPrice || process.env.A2A_PRO_PRICE || "");
|
|
322
|
+
const payTo = cleanText(payment.payTo || config?.a2aProPayTo || process.env.A2A_PRO_PAY_TO || "");
|
|
323
|
+
const expiresDaysRaw = cleanText(spec.expiresDays || config?.a2aProExpiresDays || process.env.A2A_PRO_EXPIRES_DAYS || "7");
|
|
324
|
+
const expiresDays = Number(expiresDaysRaw);
|
|
325
|
+
if (!price || !payTo) {
|
|
326
|
+
throw new Error("x402-pro task requires price and payTo metadata, or A2A_PRO_PRICE and A2A_PRO_PAY_TO");
|
|
327
|
+
}
|
|
328
|
+
if (payment.invalid === true) {
|
|
329
|
+
throw new Error(`invalid x402 payment metadata: ${payment.reason || "invalid-payment"}`);
|
|
330
|
+
}
|
|
331
|
+
if (!/^\d+(\.\d{1,6})?$/u.test(price)) {
|
|
332
|
+
throw new Error("x402-pro price must be a decimal with up to 6 fractional digits");
|
|
333
|
+
}
|
|
334
|
+
if (!/^0x[0-9a-fA-F]{40}$/u.test(payTo)) {
|
|
335
|
+
throw new Error("x402-pro payTo must be a 0x-prefixed EVM address");
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
price,
|
|
339
|
+
payTo,
|
|
340
|
+
expiresDays: Number.isFinite(expiresDays) && expiresDays > 0 && expiresDays <= 30 ? String(expiresDays) : "7",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function uploadPaidA2ADeliverable({ config, task, resultText }) {
|
|
345
|
+
const userId = cleanText(config?.a2aRelayUserId || "");
|
|
346
|
+
const apiKey = cleanText(config?.a2aApiKey || "");
|
|
347
|
+
const shareUrl = cleanText(config?.a2aShareUrl || process.env.VIVEWORKER_SHARE_URL || "https://share.viveworker.com").replace(/\/+$/u, "");
|
|
348
|
+
if (!userId || !apiKey) {
|
|
349
|
+
throw new Error("A2A credentials are required to upload paid deliverables");
|
|
350
|
+
}
|
|
351
|
+
const paidSpec = resolvePaidShareSpec(config, task);
|
|
352
|
+
const html = buildPaidDeliverableHtml({ task, resultText, paidSpec });
|
|
353
|
+
const form = new FormData();
|
|
354
|
+
const safeTaskId = cleanText(task?.id || "task").replace(/[^a-z0-9_-]+/giu, "-").slice(0, 64) || "task";
|
|
355
|
+
const file = new File(
|
|
356
|
+
[new Blob([html], { type: "text/html; charset=utf-8" })],
|
|
357
|
+
`a2a-${safeTaskId}-deliverable.html`,
|
|
358
|
+
{ type: "text/html; charset=utf-8" }
|
|
359
|
+
);
|
|
360
|
+
form.set("file", file);
|
|
361
|
+
form.set("price", paidSpec.price);
|
|
362
|
+
form.set("payTo", paidSpec.payTo);
|
|
363
|
+
form.set("expiresDays", paidSpec.expiresDays);
|
|
364
|
+
|
|
365
|
+
const response = await fetchWithTimeout(`${shareUrl}/api/upload`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"x-a2a-user": userId,
|
|
369
|
+
"x-a2a-key": apiKey,
|
|
370
|
+
},
|
|
371
|
+
body: form,
|
|
372
|
+
}, 60_000);
|
|
373
|
+
const body = await readJson(response);
|
|
374
|
+
if (!response.ok || body?.error) {
|
|
375
|
+
throw new Error(`paid deliverable upload failed: ${formatApiError(response.status, body)}`);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
url: body.url,
|
|
379
|
+
slug: body.slug,
|
|
380
|
+
price: body.price ? formatUsdcAtomic(body.price) : paidSpec.price,
|
|
381
|
+
priceAtomic: body.price || "",
|
|
382
|
+
payTo: body.payTo || paidSpec.payTo,
|
|
383
|
+
network: body.network || "",
|
|
384
|
+
expiresAtMs: body.expiresAtMs || 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function buildPaidUnlockResponse({ task, paidShare }) {
|
|
389
|
+
const deliverableType = cleanText(task?.viveworker?.deliverableType || "deliverable");
|
|
390
|
+
return [
|
|
391
|
+
`A2A Pro ${deliverableType} is ready.`,
|
|
392
|
+
"",
|
|
393
|
+
`Unlock URL: ${paidShare.url}`,
|
|
394
|
+
`Price: ${paidShare.price} USDC${paidShare.network ? ` on ${paidShare.network}` : ""}`,
|
|
395
|
+
`Pay to: ${paidShare.payTo}`,
|
|
396
|
+
"",
|
|
397
|
+
"Open the URL with an x402-compatible client or browser flow to pay and receive the result.",
|
|
398
|
+
].join("\n");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildPaidDeliverableHtml({ task, resultText, paidSpec }) {
|
|
402
|
+
const title = `A2A Pro Deliverable`;
|
|
403
|
+
const instruction = cleanText(task?.instruction || "");
|
|
404
|
+
const created = new Date().toISOString();
|
|
405
|
+
return `<!doctype html>
|
|
406
|
+
<html lang="en">
|
|
407
|
+
<head>
|
|
408
|
+
<meta charset="utf-8">
|
|
409
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
410
|
+
<title>${escapeHtml(title)}</title>
|
|
411
|
+
<style>
|
|
412
|
+
:root { color-scheme: dark; --bg: #081015; --card: #121d24; --text: #eef7fb; --muted: #9fb3bf; --line: rgba(255,255,255,.12); --accent: #7dd3fc; }
|
|
413
|
+
body { margin: 0; background: radial-gradient(circle at 20% 0%, rgba(55, 211, 153, .16), transparent 34%), var(--bg); color: var(--text); font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
414
|
+
main { max-width: 860px; margin: 0 auto; padding: 56px 22px 72px; }
|
|
415
|
+
.badge { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px; color: var(--accent); font-weight: 700; letter-spacing: .02em; }
|
|
416
|
+
h1 { font-size: clamp(34px, 7vw, 64px); line-height: .96; letter-spacing: -.055em; margin: 24px 0 16px; }
|
|
417
|
+
.meta, .prompt, .result { background: rgba(18, 29, 36, .86); border: 1px solid var(--line); border-radius: 24px; padding: 22px; margin-top: 18px; box-shadow: 0 24px 80px rgba(0,0,0,.28); }
|
|
418
|
+
.meta { color: var(--muted); }
|
|
419
|
+
.meta strong { color: var(--text); }
|
|
420
|
+
pre { white-space: pre-wrap; word-break: break-word; margin: 0; font: inherit; }
|
|
421
|
+
h2 { margin: 0 0 10px; font-size: 18px; color: var(--accent); }
|
|
422
|
+
</style>
|
|
423
|
+
</head>
|
|
424
|
+
<body>
|
|
425
|
+
<main>
|
|
426
|
+
<span class="badge">viveworker A2A Pro</span>
|
|
427
|
+
<h1>Paid agent deliverable</h1>
|
|
428
|
+
<section class="meta">
|
|
429
|
+
<div><strong>Task:</strong> ${escapeHtml(task?.id || "")}</div>
|
|
430
|
+
<div><strong>Created:</strong> ${escapeHtml(created)}</div>
|
|
431
|
+
<div><strong>Price:</strong> ${escapeHtml(paidSpec.price)} USDC</div>
|
|
432
|
+
<div><strong>Pay to:</strong> ${escapeHtml(paidSpec.payTo)}</div>
|
|
433
|
+
</section>
|
|
434
|
+
<section class="prompt">
|
|
435
|
+
<h2>Request</h2>
|
|
436
|
+
<pre>${escapeHtml(instruction)}</pre>
|
|
437
|
+
</section>
|
|
438
|
+
<section class="result">
|
|
439
|
+
<h2>Result</h2>
|
|
440
|
+
<pre>${escapeHtml(resultText)}</pre>
|
|
441
|
+
</section>
|
|
442
|
+
</main>
|
|
443
|
+
</body>
|
|
444
|
+
</html>`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function readJson(response) {
|
|
448
|
+
try {
|
|
449
|
+
return await response.json();
|
|
450
|
+
} catch {
|
|
451
|
+
return {};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function formatApiError(status, body) {
|
|
456
|
+
const error = cleanText(body?.error || body?.message || "");
|
|
457
|
+
const hint = cleanText(body?.hint || "");
|
|
458
|
+
return [`HTTP ${status}`, error, hint].filter(Boolean).join(" — ");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatUsdcAtomic(value) {
|
|
462
|
+
const raw = cleanText(value || "");
|
|
463
|
+
if (!/^\d+$/u.test(raw)) {
|
|
464
|
+
return raw;
|
|
465
|
+
}
|
|
466
|
+
const padded = raw.padStart(7, "0");
|
|
467
|
+
const whole = padded.slice(0, -6).replace(/^0+(?=\d)/u, "") || "0";
|
|
468
|
+
const frac = padded.slice(-6).replace(/0+$/u, "");
|
|
469
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function escapeHtml(value) {
|
|
473
|
+
return String(value ?? "")
|
|
474
|
+
.replace(/&/gu, "&")
|
|
475
|
+
.replace(/</gu, "<")
|
|
476
|
+
.replace(/>/gu, ">")
|
|
477
|
+
.replace(/"/gu, """);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function cleanText(value) {
|
|
481
|
+
return String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function fetchWithTimeout(url, init = {}, timeoutMs = 30_000) {
|
|
485
|
+
const controller = new AbortController();
|
|
486
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
487
|
+
try {
|
|
488
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
489
|
+
} finally {
|
|
490
|
+
clearTimeout(timer);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
247
494
|
/**
|
|
248
495
|
* Spawn `codex exec` and capture stdout+stderr.
|
|
249
496
|
*/
|
|
250
|
-
function runCodexExec(instruction, config) {
|
|
497
|
+
function runCodexExec(instruction, config, options = {}) {
|
|
251
498
|
return new Promise((resolve, reject) => {
|
|
252
499
|
const codexBin = resolveCodexBin(config);
|
|
253
|
-
const args = ["exec", "--full-auto", "--skip-git-repo-check"
|
|
500
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check"];
|
|
501
|
+
if (options.model) {
|
|
502
|
+
args.push("--model", options.model);
|
|
503
|
+
}
|
|
504
|
+
args.push(instruction);
|
|
254
505
|
const cwd = config.workspaceRoot || os.tmpdir();
|
|
255
506
|
|
|
256
507
|
const child = spawn(codexBin, args, {
|
|
@@ -303,10 +554,13 @@ function runCodexExec(instruction, config) {
|
|
|
303
554
|
/**
|
|
304
555
|
* Spawn `claude -p` and capture stdout+stderr.
|
|
305
556
|
*/
|
|
306
|
-
function runClaudeExec(instruction) {
|
|
557
|
+
function runClaudeExec(instruction, options = {}) {
|
|
307
558
|
return new Promise((resolve, reject) => {
|
|
308
559
|
const claudeBin = resolveClaudeBin();
|
|
309
560
|
const args = ["-p", instruction, "--output-format", "text"];
|
|
561
|
+
if (options.model) {
|
|
562
|
+
args.push("--model", options.model);
|
|
563
|
+
}
|
|
310
564
|
|
|
311
565
|
const child = spawn(claudeBin, args, {
|
|
312
566
|
cwd: os.tmpdir(),
|
package/scripts/a2a-handler.mjs
CHANGED
|
@@ -158,6 +158,90 @@ function extractTextFromParts(parts) {
|
|
|
158
158
|
.trim();
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function isPlainObject(value) {
|
|
162
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanMetadataText(value, maxLength = 500) {
|
|
166
|
+
return String(value ?? "")
|
|
167
|
+
.replace(/\s+/gu, " ")
|
|
168
|
+
.trim()
|
|
169
|
+
.slice(0, maxLength);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const PRICE_REGEX = /^\d+(\.\d{1,6})?$/u;
|
|
173
|
+
const ETH_ADDR_REGEX = /^0x[0-9a-fA-F]{40}$/u;
|
|
174
|
+
|
|
175
|
+
function normalizeRequestedExecutor(value) {
|
|
176
|
+
const normalized = cleanMetadataText(value, 30).toLowerCase();
|
|
177
|
+
return normalized === "codex" || normalized === "claude" || normalized === "auto" ? normalized : "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeRequestedTier(value) {
|
|
181
|
+
const normalized = cleanMetadataText(value, 50).toLowerCase();
|
|
182
|
+
if (normalized === "pro" || normalized === "pro-assisted" || normalized === "premium") {
|
|
183
|
+
return "pro";
|
|
184
|
+
}
|
|
185
|
+
if (normalized === "standard" || normalized === "free") {
|
|
186
|
+
return "standard";
|
|
187
|
+
}
|
|
188
|
+
return normalized || "";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizePaymentSpec(source) {
|
|
192
|
+
const payment = isPlainObject(source?.payment) ? source.payment : source;
|
|
193
|
+
const price = cleanMetadataText(payment?.price ?? payment?.amount ?? payment?.amountUsdc ?? "", 30);
|
|
194
|
+
const payTo = cleanMetadataText(payment?.payTo ?? payment?.pay_to ?? payment?.recipient ?? "", 80);
|
|
195
|
+
if (!price && !payTo) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (!PRICE_REGEX.test(price) || !ETH_ADDR_REGEX.test(payTo)) {
|
|
199
|
+
return {
|
|
200
|
+
enabled: false,
|
|
201
|
+
invalid: true,
|
|
202
|
+
reason: "invalid-price-or-pay-to",
|
|
203
|
+
price,
|
|
204
|
+
payTo,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
enabled: true,
|
|
209
|
+
price,
|
|
210
|
+
payTo: payTo.toLowerCase(),
|
|
211
|
+
mode: cleanMetadataText(payment?.mode || source?.settlement || "x402", 40).toLowerCase() || "x402",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function normalizeViveworkerTaskMetadata(metadata = {}) {
|
|
216
|
+
const root = isPlainObject(metadata) ? metadata : {};
|
|
217
|
+
const source = isPlainObject(root.viveworker) ? root.viveworker : root;
|
|
218
|
+
const mode = cleanMetadataText(source.mode || source.flow || "", 80).toLowerCase();
|
|
219
|
+
const requestedTier = normalizeRequestedTier(source.requestedTier || source.tier || source.qualityTier || source.modelTier);
|
|
220
|
+
const requestedExecutor = normalizeRequestedExecutor(source.requestedExecutor || source.executor);
|
|
221
|
+
const requestedModel = cleanMetadataText(source.requestedModel || source.model || "", 80);
|
|
222
|
+
const deliverableType = cleanMetadataText(source.deliverableType || source.deliverable || "research brief", 120);
|
|
223
|
+
const payment = normalizePaymentSpec(source);
|
|
224
|
+
const paidDeliverable =
|
|
225
|
+
mode === "x402-pro" ||
|
|
226
|
+
mode === "pay-per-unlock" ||
|
|
227
|
+
mode === "paid-unlock" ||
|
|
228
|
+
Boolean(payment);
|
|
229
|
+
|
|
230
|
+
if (!mode && !requestedTier && !requestedExecutor && !requestedModel && !paidDeliverable) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
mode: mode || (paidDeliverable ? "x402-pro" : ""),
|
|
236
|
+
requestedTier,
|
|
237
|
+
requestedExecutor,
|
|
238
|
+
requestedModel,
|
|
239
|
+
deliverableType,
|
|
240
|
+
paidDeliverable,
|
|
241
|
+
payment,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
161
245
|
function buildTaskResponse(task) {
|
|
162
246
|
return {
|
|
163
247
|
id: task.id,
|
|
@@ -269,6 +353,7 @@ async function handleMessageSend({
|
|
|
269
353
|
const contextId = params.contextId || taskId();
|
|
270
354
|
const token = historyToken(`a2a_task:${id}`);
|
|
271
355
|
const now = Date.now();
|
|
356
|
+
const viveworker = normalizeViveworkerTaskMetadata(message.metadata || params.metadata || {});
|
|
272
357
|
|
|
273
358
|
const task = {
|
|
274
359
|
id,
|
|
@@ -279,6 +364,8 @@ async function handleMessageSend({
|
|
|
279
364
|
messages: [message],
|
|
280
365
|
artifacts: [],
|
|
281
366
|
instruction,
|
|
367
|
+
metadata: message.metadata || params.metadata || {},
|
|
368
|
+
viveworker,
|
|
282
369
|
callerInfo: {
|
|
283
370
|
ip: req.socket?.remoteAddress || "",
|
|
284
371
|
userAgent: req.headers["user-agent"] || "",
|
|
@@ -311,6 +398,7 @@ async function handleMessageSend({
|
|
|
311
398
|
summary: cleanText(instruction).slice(0, 160),
|
|
312
399
|
instruction,
|
|
313
400
|
messageText: instruction,
|
|
401
|
+
viveworker,
|
|
314
402
|
createdAtMs: now,
|
|
315
403
|
readOnly: false,
|
|
316
404
|
provider: "a2a",
|