viveworker 0.8.0 → 0.8.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/.agents/plugins/marketplace.json +20 -0
- package/README.md +141 -0
- package/package.json +7 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -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/lib/markdown-render.mjs +128 -1
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +1504 -128
- package/scripts/viveworker.mjs +262 -1
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +162 -0
- package/web/app.js +1164 -101
- package/web/build-id.js +1 -0
- package/web/i18n.js +123 -15
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +2 -2
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +16 -6
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
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",
|
|
@@ -14,6 +14,7 @@ import os from "node:os";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { promises as fs, readFileSync } from "node:fs";
|
|
16
16
|
import { upsertEnvText } from "./lib/pairing.mjs";
|
|
17
|
+
import { normalizeViveworkerTaskMetadata } from "./a2a-handler.mjs";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Constants
|
|
@@ -248,6 +249,8 @@ async function ingestRelayTask({ relayTask, config, runtime, state, helpers }) {
|
|
|
248
249
|
const token = historyToken(`a2a_task:${relayTask.id}`);
|
|
249
250
|
const instruction = cleanText(relayTask.instruction || "");
|
|
250
251
|
const now = Date.now();
|
|
252
|
+
const relayMetadata = relayTask.metadata || relayTask.messages?.[0]?.metadata || {};
|
|
253
|
+
const viveworker = normalizeViveworkerTaskMetadata(relayMetadata);
|
|
251
254
|
|
|
252
255
|
const task = {
|
|
253
256
|
id: relayTask.id,
|
|
@@ -258,6 +261,8 @@ async function ingestRelayTask({ relayTask, config, runtime, state, helpers }) {
|
|
|
258
261
|
messages: relayTask.messages || [],
|
|
259
262
|
artifacts: [],
|
|
260
263
|
instruction,
|
|
264
|
+
metadata: relayMetadata,
|
|
265
|
+
viveworker,
|
|
261
266
|
callerInfo: relayTask.callerInfo || {},
|
|
262
267
|
createdAtMs: relayTask.createdAtMs || now,
|
|
263
268
|
updatedAtMs: now,
|
|
@@ -289,6 +294,7 @@ async function ingestRelayTask({ relayTask, config, runtime, state, helpers }) {
|
|
|
289
294
|
title: `A2A: ${instruction.slice(0, 80)}`,
|
|
290
295
|
summary: instruction.slice(0, 160),
|
|
291
296
|
messageText: instruction,
|
|
297
|
+
viveworker,
|
|
292
298
|
createdAtMs: relayTask.createdAtMs || now,
|
|
293
299
|
readOnly: false,
|
|
294
300
|
provider: "a2a",
|
|
@@ -170,15 +170,125 @@ function renderBlockquote(lines) {
|
|
|
170
170
|
|
|
171
171
|
function renderCodeBlock(lines) {
|
|
172
172
|
const [fence, ...rest] = lines;
|
|
173
|
+
// parseBlocks pushes the closing fence onto the block before breaking out
|
|
174
|
+
// of the loop, so `rest` ends with that closing line whenever the fence is
|
|
175
|
+
// properly closed. Strip it so the rendered <pre><code>...</code></pre>
|
|
176
|
+
// doesn't show the literal "```" as the last line of the code body.
|
|
177
|
+
const body = [...rest];
|
|
178
|
+
if (body.length > 0 && /^```\s*$/u.test(body[body.length - 1])) {
|
|
179
|
+
body.pop();
|
|
180
|
+
}
|
|
173
181
|
const language = fence.replace(/^```/u, "").trim();
|
|
174
182
|
const className = language ? ` class="language-${escapeHtml(language)}"` : "";
|
|
175
|
-
return `<pre><code${className}>${escapeHtml(
|
|
183
|
+
return `<pre><code${className}>${escapeHtml(body.join("\n"))}</code></pre>`;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
function renderParagraph(lines) {
|
|
179
187
|
return `<p>${lines.map((line) => renderInline(line.trim())).join("<br>")}</p>`;
|
|
180
188
|
}
|
|
181
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Pipe-style markdown tables
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
//
|
|
194
|
+
// | header 1 | header 2 |
|
|
195
|
+
// |----------|---------:|
|
|
196
|
+
// | cell | 42 |
|
|
197
|
+
//
|
|
198
|
+
// Detection requires both a header row (single line starting AND ending with
|
|
199
|
+
// `|`) and a separator row immediately below it whose cells are
|
|
200
|
+
// `:?-{2,}:?` (with optional alignment markers). Anything else falls through
|
|
201
|
+
// to the paragraph fallback so a stray `| not a table |` line still
|
|
202
|
+
// renders as plain text.
|
|
203
|
+
//
|
|
204
|
+
// Cells go through `renderInline` so they pick up the existing escape /
|
|
205
|
+
// inline-formatting pipeline; we never inject raw HTML.
|
|
206
|
+
|
|
207
|
+
function isTableRowLine(line) {
|
|
208
|
+
if (typeof line !== "string") return false;
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
return trimmed.length >= 2 && trimmed.startsWith("|") && trimmed.endsWith("|");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function splitTableCells(line) {
|
|
214
|
+
// Strip the leading + trailing pipes; honour `\|` as an escaped literal.
|
|
215
|
+
const trimmed = line.trim().replace(/^\|/u, "").replace(/\|$/u, "");
|
|
216
|
+
const cells = [];
|
|
217
|
+
let current = "";
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i < trimmed.length) {
|
|
220
|
+
if (trimmed[i] === "\\" && trimmed[i + 1] === "|") {
|
|
221
|
+
current += "|";
|
|
222
|
+
i += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (trimmed[i] === "|") {
|
|
226
|
+
cells.push(current);
|
|
227
|
+
current = "";
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
current += trimmed[i];
|
|
232
|
+
i += 1;
|
|
233
|
+
}
|
|
234
|
+
cells.push(current);
|
|
235
|
+
return cells.map((cell) => cell.trim());
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isTableSeparatorLine(line) {
|
|
239
|
+
if (!isTableRowLine(line)) return false;
|
|
240
|
+
const cells = splitTableCells(line);
|
|
241
|
+
if (cells.length === 0) return false;
|
|
242
|
+
return cells.every((cell) => /^:?-{2,}:?$/u.test(cell));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseTableAlignments(separatorLine) {
|
|
246
|
+
return splitTableCells(separatorLine).map((cell) => {
|
|
247
|
+
const left = cell.startsWith(":");
|
|
248
|
+
const right = cell.endsWith(":");
|
|
249
|
+
if (left && right) return "center";
|
|
250
|
+
if (right) return "right";
|
|
251
|
+
if (left) return "left";
|
|
252
|
+
return "";
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderTableCell(cell, align, tag) {
|
|
257
|
+
const styleAttr = align ? ` style="text-align:${align}"` : "";
|
|
258
|
+
return `<${tag}${styleAttr}>${renderInline(cell)}</${tag}>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderTable(headerLine, separatorLine, dataLines) {
|
|
262
|
+
const alignments = parseTableAlignments(separatorLine);
|
|
263
|
+
const headerCells = splitTableCells(headerLine);
|
|
264
|
+
// Pad alignments out to header width so any data row that's wider still
|
|
265
|
+
// gets a sensible alignment default.
|
|
266
|
+
while (alignments.length < headerCells.length) alignments.push("");
|
|
267
|
+
|
|
268
|
+
const headerHtml = headerCells
|
|
269
|
+
.map((cell, i) => renderTableCell(cell, alignments[i] || "", "th"))
|
|
270
|
+
.join("");
|
|
271
|
+
const bodyHtml = dataLines
|
|
272
|
+
.map((row) => {
|
|
273
|
+
const cells = splitTableCells(row);
|
|
274
|
+
const cellsHtml = cells
|
|
275
|
+
.map((cell, i) => renderTableCell(cell, alignments[i] || "", "td"))
|
|
276
|
+
.join("");
|
|
277
|
+
return `<tr>${cellsHtml}</tr>`;
|
|
278
|
+
})
|
|
279
|
+
.join("");
|
|
280
|
+
|
|
281
|
+
return `<table><thead><tr>${headerHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function looksLikeTableStart(lines, index) {
|
|
285
|
+
return (
|
|
286
|
+
isTableRowLine(lines[index]) &&
|
|
287
|
+
index + 1 < lines.length &&
|
|
288
|
+
isTableSeparatorLine(lines[index + 1])
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
182
292
|
function parseBlocks(markdown) {
|
|
183
293
|
const lines = markdown.replace(/\r\n/gu, "\n").trim().split("\n");
|
|
184
294
|
const blocks = [];
|
|
@@ -241,6 +351,19 @@ function parseBlocks(markdown) {
|
|
|
241
351
|
continue;
|
|
242
352
|
}
|
|
243
353
|
|
|
354
|
+
if (looksLikeTableStart(lines, index)) {
|
|
355
|
+
const headerLine = lines[index];
|
|
356
|
+
const separatorLine = lines[index + 1];
|
|
357
|
+
const dataLines = [];
|
|
358
|
+
index += 2;
|
|
359
|
+
while (index < lines.length && isTableRowLine(lines[index])) {
|
|
360
|
+
dataLines.push(lines[index]);
|
|
361
|
+
index += 1;
|
|
362
|
+
}
|
|
363
|
+
blocks.push(renderTable(headerLine, separatorLine, dataLines));
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
244
367
|
const block = [];
|
|
245
368
|
while (index < lines.length) {
|
|
246
369
|
const current = lines[index];
|
|
@@ -250,6 +373,10 @@ function parseBlocks(markdown) {
|
|
|
250
373
|
if (isUnorderedList(current) || isOrderedList(current) || /^(?:---|\*\*\*|___)\s*$/u.test(current)) {
|
|
251
374
|
break;
|
|
252
375
|
}
|
|
376
|
+
if (looksLikeTableStart(lines, index)) {
|
|
377
|
+
// Don't swallow a table-start row into the previous paragraph.
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
253
380
|
block.push(current);
|
|
254
381
|
index += 1;
|
|
255
382
|
}
|