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
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
15
|
+
const SERVER_NAME = "viveworker";
|
|
16
|
+
const DEFAULT_CONFIG_FILE = path.join(os.homedir(), ".viveworker", "config.env");
|
|
17
|
+
const A2A_ENV_FILE = path.join(os.homedir(), ".viveworker", "a2a.env");
|
|
18
|
+
const A2A_TARGETS_FILE = path.join(os.homedir(), ".viveworker", "a2a-targets.json");
|
|
19
|
+
const DEFAULT_A2A_RELAY_URL = "https://a2a.viveworker.com";
|
|
20
|
+
const BRIDGE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
21
|
+
const SHORT_TIMEOUT_MS = 15_000;
|
|
22
|
+
const SHARE_FILE_EXTENSIONS = new Set([".html", ".htm", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".csv"]);
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
const viveworkerCli = process.env.VIVEWORKER_MCP_CLI || path.join(__dirname, "viveworker.mjs");
|
|
26
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
27
|
+
|
|
28
|
+
export async function runMcpCli(args = []) {
|
|
29
|
+
const cmd = args[0] || "serve";
|
|
30
|
+
if (cmd === "config") {
|
|
31
|
+
printConfigSnippets();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
35
|
+
printHelp();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (cmd !== "serve") {
|
|
39
|
+
throw new Error(`Unknown mcp command: ${cmd}`);
|
|
40
|
+
}
|
|
41
|
+
await runMcpServer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runMcpServer() {
|
|
45
|
+
const server = new ViveworkerMcpServer();
|
|
46
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
47
|
+
for await (const line of rl) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (!trimmed) continue;
|
|
50
|
+
let message;
|
|
51
|
+
try {
|
|
52
|
+
message = JSON.parse(trimmed);
|
|
53
|
+
} catch {
|
|
54
|
+
writeRpc({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await server.handleMessage(message);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (message && Object.prototype.hasOwnProperty.call(message, "id")) {
|
|
61
|
+
writeRpc({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
id: message.id ?? null,
|
|
64
|
+
error: {
|
|
65
|
+
code: Number(error?.rpcCode) || -32603,
|
|
66
|
+
message: error?.message || "Internal error",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
log(`notification error: ${error?.message || error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class ViveworkerMcpServer {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.initialized = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async handleMessage(message) {
|
|
82
|
+
if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
83
|
+
if (message && Object.prototype.hasOwnProperty.call(message, "id")) {
|
|
84
|
+
writeError(message.id, -32600, "Invalid Request");
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!Object.prototype.hasOwnProperty.call(message, "id")) {
|
|
90
|
+
if (message.method === "notifications/initialized") {
|
|
91
|
+
this.initialized = true;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const id = message.id;
|
|
97
|
+
switch (message.method) {
|
|
98
|
+
case "initialize":
|
|
99
|
+
return writeResult(id, await this.initialize(message.params || {}));
|
|
100
|
+
case "ping":
|
|
101
|
+
return writeResult(id, {});
|
|
102
|
+
case "tools/list":
|
|
103
|
+
return writeResult(id, { tools: TOOLS });
|
|
104
|
+
case "tools/call":
|
|
105
|
+
return writeResult(id, await callTool(message.params || {}));
|
|
106
|
+
case "prompts/list":
|
|
107
|
+
return writeResult(id, { prompts: PROMPTS.map((prompt) => prompt.definition) });
|
|
108
|
+
case "prompts/get":
|
|
109
|
+
return writeResult(id, getPrompt(message.params || {}));
|
|
110
|
+
default:
|
|
111
|
+
return writeError(id, -32601, `Method not found: ${message.method}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async initialize(params) {
|
|
116
|
+
const requested = String(params?.protocolVersion || "");
|
|
117
|
+
return {
|
|
118
|
+
protocolVersion: requested || PROTOCOL_VERSION,
|
|
119
|
+
capabilities: {
|
|
120
|
+
tools: { listChanged: false },
|
|
121
|
+
prompts: { listChanged: false },
|
|
122
|
+
},
|
|
123
|
+
serverInfo: {
|
|
124
|
+
name: SERVER_NAME,
|
|
125
|
+
title: "viveworker MCP",
|
|
126
|
+
version: await readPackageVersion(),
|
|
127
|
+
websiteUrl: "https://viveworker.com",
|
|
128
|
+
},
|
|
129
|
+
instructions:
|
|
130
|
+
"Use viveworker tools when you need to notify, ask, request approval, share a file, hand off context, or delegate an A2A task through the user's paired mobile control plane.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function callTool(params) {
|
|
136
|
+
const name = String(params?.name || "");
|
|
137
|
+
const args = isPlainObject(params?.arguments) ? params.arguments : {};
|
|
138
|
+
switch (name) {
|
|
139
|
+
case "viveworker_status":
|
|
140
|
+
return toolOk(await bridgeEvent({ eventType: "status" }, SHORT_TIMEOUT_MS));
|
|
141
|
+
case "viveworker_notify":
|
|
142
|
+
return toolOk(await toolNotify(args));
|
|
143
|
+
case "viveworker_ask":
|
|
144
|
+
return toolOk(await toolAsk(args));
|
|
145
|
+
case "viveworker_request_approval":
|
|
146
|
+
return toolOk(await toolRequestApproval(args));
|
|
147
|
+
case "viveworker_share_file":
|
|
148
|
+
return toolOk(await toolShareFile(args));
|
|
149
|
+
case "viveworker_thread_share":
|
|
150
|
+
return toolOk(await toolThreadShare(args));
|
|
151
|
+
case "viveworker_send_a2a_task":
|
|
152
|
+
return toolOk(await toolSendA2ATask(args));
|
|
153
|
+
default:
|
|
154
|
+
throw rpcInvalidParams(`Unknown tool: ${name}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function toolNotify(args) {
|
|
159
|
+
const title = requiredString(args.title, "title");
|
|
160
|
+
const message = requiredString(args.message, "message");
|
|
161
|
+
return bridgeEvent({
|
|
162
|
+
eventType: "notify",
|
|
163
|
+
title,
|
|
164
|
+
message,
|
|
165
|
+
threadLabel: optionalString(args.threadLabel) || "MCP",
|
|
166
|
+
}, SHORT_TIMEOUT_MS);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function toolAsk(args) {
|
|
170
|
+
const question = requiredString(args.question, "question");
|
|
171
|
+
const options = normalizeOptionArgs(args.options);
|
|
172
|
+
return bridgeEvent({
|
|
173
|
+
eventType: "choice_request",
|
|
174
|
+
title: optionalString(args.title) || "MCP question",
|
|
175
|
+
question,
|
|
176
|
+
options,
|
|
177
|
+
allowFreeform: args.allowFreeform !== false,
|
|
178
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
179
|
+
}, clampTimeout(args.timeoutMs));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function toolRequestApproval(args) {
|
|
183
|
+
const title = requiredString(args.title, "title");
|
|
184
|
+
const message = requiredString(args.message, "message");
|
|
185
|
+
const approvalKind = optionalString(args.approvalKind) || "mcp";
|
|
186
|
+
return bridgeEvent({
|
|
187
|
+
eventType: "approval_request",
|
|
188
|
+
title,
|
|
189
|
+
message,
|
|
190
|
+
approvalKind,
|
|
191
|
+
fileRefs: normalizeStringArray(args.fileRefs),
|
|
192
|
+
diffText: optionalString(args.diffText),
|
|
193
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
194
|
+
}, clampTimeout(args.timeoutMs));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function toolShareFile(args) {
|
|
198
|
+
const requestedPath = requiredString(args.path, "path");
|
|
199
|
+
const workspaceRoot = optionalString(args.workspaceRoot) || process.env.VIVEWORKER_MCP_WORKSPACE_ROOT || process.cwd();
|
|
200
|
+
const file = await validateSharePath(requestedPath, workspaceRoot);
|
|
201
|
+
const stat = await fs.stat(file.realPath);
|
|
202
|
+
const approval = await bridgeEvent({
|
|
203
|
+
eventType: "approval_request",
|
|
204
|
+
title: "Share file with viveworker File Share",
|
|
205
|
+
message: [
|
|
206
|
+
"An MCP client wants to upload a local file to viveworker File Share.",
|
|
207
|
+
"",
|
|
208
|
+
`File: ${file.displayPath}`,
|
|
209
|
+
`Size: ${stat.size} bytes`,
|
|
210
|
+
"",
|
|
211
|
+
"Approve only if this file is safe to send outside this Mac.",
|
|
212
|
+
].join("\n"),
|
|
213
|
+
approvalKind: "file_share",
|
|
214
|
+
fileRefs: [file.displayPath],
|
|
215
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
216
|
+
}, clampTimeout(args.timeoutMs));
|
|
217
|
+
if (!approval.approved) {
|
|
218
|
+
return { approved: false, decision: approval.decision || "rejected" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const uploadArgs = ["share", "upload", file.realPath, "--json"];
|
|
222
|
+
const password = optionalString(args.password);
|
|
223
|
+
const expiresDays = optionalString(args.expiresDays ?? args["expires-days"]);
|
|
224
|
+
if (password) uploadArgs.push("--password", password);
|
|
225
|
+
if (expiresDays) uploadArgs.push("--expires-days", expiresDays);
|
|
226
|
+
const upload = await runViveworkerCliJson(uploadArgs, 90_000);
|
|
227
|
+
return { approved: true, share: upload };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function toolThreadShare(args) {
|
|
231
|
+
const content = requiredString(args.content, "content");
|
|
232
|
+
const targetConversationId = optionalString(args.targetConversationId);
|
|
233
|
+
const targetTool = optionalString(args.targetTool);
|
|
234
|
+
if (!targetConversationId && !targetTool) {
|
|
235
|
+
throw rpcInvalidParams("targetConversationId or targetTool is required");
|
|
236
|
+
}
|
|
237
|
+
const result = await postBridgeJson("/api/threads/share", {
|
|
238
|
+
shareType: optionalString(args.shareType) || "message",
|
|
239
|
+
content,
|
|
240
|
+
sourceTool: "mcp",
|
|
241
|
+
sourceLabel: optionalString(args.sourceLabel) || "MCP",
|
|
242
|
+
targetConversationId,
|
|
243
|
+
targetTool,
|
|
244
|
+
targetCwd: optionalString(args.targetCwd),
|
|
245
|
+
contextFiles: normalizeStringArray(args.contextFiles),
|
|
246
|
+
}, SHORT_TIMEOUT_MS);
|
|
247
|
+
return result.body;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function toolSendA2ATask(args) {
|
|
251
|
+
const target = requiredString(args.target, "target");
|
|
252
|
+
const instruction = requiredString(args.instruction, "instruction");
|
|
253
|
+
const resolved = await resolveA2ATarget(target);
|
|
254
|
+
const approval = await bridgeEvent({
|
|
255
|
+
eventType: "approval_request",
|
|
256
|
+
title: `Send A2A task to ${target}`,
|
|
257
|
+
message: [
|
|
258
|
+
"An MCP client wants to send this task to a registered A2A target.",
|
|
259
|
+
"",
|
|
260
|
+
`Target: ${target}`,
|
|
261
|
+
"",
|
|
262
|
+
instruction,
|
|
263
|
+
].join("\n"),
|
|
264
|
+
approvalKind: "a2a_task",
|
|
265
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
266
|
+
}, clampTimeout(args.timeoutMs));
|
|
267
|
+
if (!approval.approved) {
|
|
268
|
+
return { approved: false, decision: approval.decision || "rejected" };
|
|
269
|
+
}
|
|
270
|
+
const result = await sendA2AMessage(resolved, instruction, {
|
|
271
|
+
metadata: isPlainObject(args.metadata) ? args.metadata : {},
|
|
272
|
+
});
|
|
273
|
+
return { approved: true, target, result };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function bridgeEvent(body, timeoutMs) {
|
|
277
|
+
const res = await postBridgeJson("/api/providers/mcp/events", body, timeoutMs);
|
|
278
|
+
return res.body;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function postBridgeJson(pathname, body, timeoutMs) {
|
|
282
|
+
const bridge = await resolveBridgeConfig();
|
|
283
|
+
if (!bridge.baseUrl || !bridge.sessionSecret) {
|
|
284
|
+
throw new Error("viveworker bridge config missing. Run `npx viveworker setup` first.");
|
|
285
|
+
}
|
|
286
|
+
const endpoint = `${bridge.baseUrl}${pathname}`;
|
|
287
|
+
const result = await httpJson(endpoint, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: {
|
|
290
|
+
"content-type": "application/json",
|
|
291
|
+
"x-viveworker-hook-secret": bridge.sessionSecret,
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify(body),
|
|
294
|
+
timeoutMs,
|
|
295
|
+
rejectUnauthorized: shouldVerifyTls(endpoint),
|
|
296
|
+
});
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
throw new Error(`bridge request failed (${result.status}): ${formatBridgeError(result.body)}`);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function resolveBridgeConfig() {
|
|
304
|
+
const envText = await readOptionalFile(process.env.VIVEWORKER_CONFIG_ENV || DEFAULT_CONFIG_FILE);
|
|
305
|
+
const publicBaseUrl = (
|
|
306
|
+
process.env.VIVEWORKER_MCP_BRIDGE_URL ||
|
|
307
|
+
process.env.VIVEWORKER_APPROVAL_BRIDGE_URL ||
|
|
308
|
+
envValue(envText, "VIVEWORKER_MCP_BRIDGE_URL") ||
|
|
309
|
+
envValue(envText, "VIVEWORKER_APPROVAL_BRIDGE_URL") ||
|
|
310
|
+
envValue(envText, "NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL") ||
|
|
311
|
+
""
|
|
312
|
+
).replace(/\/$/u, "");
|
|
313
|
+
const port = process.env.NATIVE_APPROVAL_SERVER_PORT || envValue(envText, "NATIVE_APPROVAL_SERVER_PORT") || "";
|
|
314
|
+
const protocol = publicBaseUrl.startsWith("https:") ? "https" : "http";
|
|
315
|
+
const baseUrl = (
|
|
316
|
+
process.env.VIVEWORKER_MCP_BRIDGE_URL ||
|
|
317
|
+
process.env.VIVEWORKER_APPROVAL_BRIDGE_URL ||
|
|
318
|
+
(port ? `${protocol}://127.0.0.1:${port}` : publicBaseUrl)
|
|
319
|
+
).replace(/\/$/u, "");
|
|
320
|
+
const sessionSecret = (
|
|
321
|
+
process.env.VIVEWORKER_MCP_SESSION_SECRET ||
|
|
322
|
+
process.env.SESSION_SECRET ||
|
|
323
|
+
envValue(envText, "SESSION_SECRET") ||
|
|
324
|
+
""
|
|
325
|
+
).trim();
|
|
326
|
+
return { baseUrl, sessionSecret };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function httpJson(endpoint, options) {
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = new URL(endpoint);
|
|
334
|
+
} catch {
|
|
335
|
+
resolve({ ok: false, status: 0, body: { error: "invalid-url" } });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const payload = options.body || "";
|
|
339
|
+
const isHttps = parsed.protocol === "https:";
|
|
340
|
+
const req = (isHttps ? https : http).request({
|
|
341
|
+
hostname: parsed.hostname,
|
|
342
|
+
port: parsed.port ? Number(parsed.port) : isHttps ? 443 : 80,
|
|
343
|
+
path: parsed.pathname + parsed.search,
|
|
344
|
+
method: options.method || "GET",
|
|
345
|
+
headers: {
|
|
346
|
+
...(options.headers || {}),
|
|
347
|
+
...(payload ? { "content-length": Buffer.byteLength(payload) } : {}),
|
|
348
|
+
},
|
|
349
|
+
rejectUnauthorized: options.rejectUnauthorized !== false,
|
|
350
|
+
}, (res) => {
|
|
351
|
+
let text = "";
|
|
352
|
+
res.on("data", (chunk) => { text += chunk; });
|
|
353
|
+
res.on("end", () => {
|
|
354
|
+
let body = {};
|
|
355
|
+
try {
|
|
356
|
+
body = text ? JSON.parse(text) : {};
|
|
357
|
+
} catch {
|
|
358
|
+
body = { error: text.slice(0, 500) };
|
|
359
|
+
}
|
|
360
|
+
resolve({ ok: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300, status: res.statusCode || 0, body });
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
const timer = setTimeout(() => {
|
|
364
|
+
req.destroy(new Error("request-timeout"));
|
|
365
|
+
}, Math.max(1000, Number(options.timeoutMs) || SHORT_TIMEOUT_MS));
|
|
366
|
+
req.on("close", () => clearTimeout(timer));
|
|
367
|
+
req.on("error", (error) => {
|
|
368
|
+
clearTimeout(timer);
|
|
369
|
+
resolve({ ok: false, status: 0, body: { error: error.message || "request-failed" } });
|
|
370
|
+
});
|
|
371
|
+
if (payload) req.write(payload);
|
|
372
|
+
req.end();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function validateSharePath(filePath, workspaceRoot) {
|
|
377
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
378
|
+
const candidate = path.resolve(String(filePath || ""));
|
|
379
|
+
const [rootReal, fileReal] = await Promise.all([
|
|
380
|
+
fs.realpath(root),
|
|
381
|
+
fs.realpath(candidate),
|
|
382
|
+
]);
|
|
383
|
+
const rel = path.relative(rootReal, fileReal);
|
|
384
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
385
|
+
throw new Error("share_file is limited to the current workspace root");
|
|
386
|
+
}
|
|
387
|
+
const stat = await fs.stat(fileReal);
|
|
388
|
+
if (!stat.isFile()) {
|
|
389
|
+
throw new Error("share_file path must be a regular file");
|
|
390
|
+
}
|
|
391
|
+
assertNotSensitivePath(rel || path.basename(fileReal));
|
|
392
|
+
const ext = path.extname(fileReal).toLowerCase();
|
|
393
|
+
if (!SHARE_FILE_EXTENSIONS.has(ext)) {
|
|
394
|
+
throw new Error(`share_file accepts only ${Array.from(SHARE_FILE_EXTENSIONS).join(" / ")} files. Got: ${ext || "(no extension)"}`);
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
realPath: fileReal,
|
|
398
|
+
displayPath: path.join(rootReal, rel),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function assertNotSensitivePath(relPath) {
|
|
403
|
+
const parts = String(relPath || "").split(/[\\/]+/u).map((part) => part.toLowerCase());
|
|
404
|
+
const deniedExact = new Set([
|
|
405
|
+
".env",
|
|
406
|
+
".env.local",
|
|
407
|
+
".env.production",
|
|
408
|
+
".env.development",
|
|
409
|
+
".npmrc",
|
|
410
|
+
".pypirc",
|
|
411
|
+
".netrc",
|
|
412
|
+
"id_rsa",
|
|
413
|
+
"id_dsa",
|
|
414
|
+
"id_ecdsa",
|
|
415
|
+
"id_ed25519",
|
|
416
|
+
]);
|
|
417
|
+
for (const part of parts) {
|
|
418
|
+
if (!part) continue;
|
|
419
|
+
if (part === ".ssh" || part === ".aws" || part === ".gnupg") {
|
|
420
|
+
throw new Error("share_file refuses credential directories");
|
|
421
|
+
}
|
|
422
|
+
if (deniedExact.has(part) || part.endsWith(".pem") || part.endsWith(".key") || part.includes("secret") || part.includes("credential") || part.includes("private-key")) {
|
|
423
|
+
throw new Error("share_file refuses paths that look like secrets or credentials");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function runViveworkerCliJson(args, timeoutMs) {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const child = spawn(process.execPath, [viveworkerCli, ...args], {
|
|
431
|
+
cwd: packageRoot,
|
|
432
|
+
env: process.env,
|
|
433
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
434
|
+
shell: false,
|
|
435
|
+
});
|
|
436
|
+
let stdout = "";
|
|
437
|
+
let stderr = "";
|
|
438
|
+
const timer = setTimeout(() => {
|
|
439
|
+
child.kill("SIGTERM");
|
|
440
|
+
reject(new Error("viveworker CLI timed out"));
|
|
441
|
+
}, timeoutMs);
|
|
442
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
443
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
444
|
+
child.on("error", (error) => {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
reject(error);
|
|
447
|
+
});
|
|
448
|
+
child.on("close", (code) => {
|
|
449
|
+
clearTimeout(timer);
|
|
450
|
+
if (code !== 0) {
|
|
451
|
+
reject(new Error((stderr || stdout || `viveworker CLI failed with code ${code}`).trim()));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
resolve(JSON.parse(stdout));
|
|
456
|
+
} catch {
|
|
457
|
+
reject(new Error(`viveworker CLI returned non-JSON: ${stdout.slice(0, 200)}`));
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function resolveA2ATarget(alias) {
|
|
464
|
+
const targets = await readA2ATargets();
|
|
465
|
+
const entry = targets[alias];
|
|
466
|
+
if (!entry || typeof entry !== "object") {
|
|
467
|
+
throw new Error(`A2A target not found: ${alias}. Add it to ${A2A_TARGETS_FILE}.`);
|
|
468
|
+
}
|
|
469
|
+
const url = String(entry.url || entry.endpoint || "").trim().replace(/\/$/u, "");
|
|
470
|
+
if (!url || !/^https?:\/\//u.test(url)) {
|
|
471
|
+
throw new Error(`A2A target ${alias} is missing a valid url`);
|
|
472
|
+
}
|
|
473
|
+
assertSafeA2ATargetUrl(url, alias);
|
|
474
|
+
if (entry.apiKey) {
|
|
475
|
+
throw new Error(`A2A target ${alias} uses inline apiKey; use apiKeyEnv instead`);
|
|
476
|
+
}
|
|
477
|
+
const apiKeyEnv = String(entry.apiKeyEnv || defaultA2AApiKeyEnv(alias)).trim();
|
|
478
|
+
const a2aEnvText = await readOptionalFile(process.env.VIVEWORKER_MCP_A2A_ENV_FILE || A2A_ENV_FILE);
|
|
479
|
+
const apiKey = apiKeyEnv ? String(process.env[apiKeyEnv] || envValue(a2aEnvText, apiKeyEnv) || "").trim() : "";
|
|
480
|
+
return { url, apiKey, apiKeyEnv };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function readA2ATargets() {
|
|
484
|
+
const targetsFile = process.env.VIVEWORKER_MCP_A2A_TARGETS_FILE || A2A_TARGETS_FILE;
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(await fs.readFile(targetsFile, "utf8"));
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (error?.code === "ENOENT") return {};
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function defaultA2AApiKeyEnv(alias) {
|
|
494
|
+
return `VIVEWORKER_A2A_TARGET_${String(alias || "").toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_KEY`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function assertSafeA2ATargetUrl(url, alias) {
|
|
498
|
+
let parsed;
|
|
499
|
+
try {
|
|
500
|
+
parsed = new URL(url);
|
|
501
|
+
} catch {
|
|
502
|
+
throw new Error(`A2A target ${alias} is missing a valid url`);
|
|
503
|
+
}
|
|
504
|
+
if (parsed.protocol === "https:") {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
throw new Error(`A2A target ${alias} must use https, except localhost/127.0.0.1 development targets`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function shouldVerifyTls(endpoint) {
|
|
514
|
+
try {
|
|
515
|
+
const parsed = new URL(endpoint);
|
|
516
|
+
if (parsed.protocol !== "https:") return true;
|
|
517
|
+
return !isLocalBridgeHostname(parsed.hostname);
|
|
518
|
+
} catch {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isLocalBridgeHostname(hostname) {
|
|
524
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
525
|
+
return isLoopbackHostname(normalized) || normalized.endsWith(".local");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function isLoopbackHostname(hostname) {
|
|
529
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
530
|
+
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function sendA2AMessage(target, instruction, options = {}) {
|
|
534
|
+
const id = crypto.randomUUID();
|
|
535
|
+
const headers = { "content-type": "application/json" };
|
|
536
|
+
if (target.apiKey) headers["x-a2a-key"] = target.apiKey;
|
|
537
|
+
const result = await httpJson(target.url, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers,
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
jsonrpc: "2.0",
|
|
542
|
+
id,
|
|
543
|
+
method: "message/send",
|
|
544
|
+
params: {
|
|
545
|
+
message: {
|
|
546
|
+
role: "user",
|
|
547
|
+
parts: [{ type: "text", text: instruction }],
|
|
548
|
+
metadata: options.metadata || {},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
}),
|
|
552
|
+
timeoutMs: 30_000,
|
|
553
|
+
});
|
|
554
|
+
if (!result.ok || result.body?.error) {
|
|
555
|
+
throw new Error(`A2A task failed (${result.status}): ${formatBridgeError(result.body)}`);
|
|
556
|
+
}
|
|
557
|
+
return result.body.result ?? result.body;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function normalizeOptionArgs(value) {
|
|
561
|
+
if (!Array.isArray(value)) return [];
|
|
562
|
+
return value
|
|
563
|
+
.map((entry, index) => {
|
|
564
|
+
if (typeof entry === "string") return { label: entry };
|
|
565
|
+
if (!isPlainObject(entry)) return null;
|
|
566
|
+
const label = optionalString(entry.label) || optionalString(entry.title) || `Option ${index + 1}`;
|
|
567
|
+
return {
|
|
568
|
+
label,
|
|
569
|
+
description: optionalString(entry.description) || optionalString(entry.detail),
|
|
570
|
+
};
|
|
571
|
+
})
|
|
572
|
+
.filter(Boolean);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeStringArray(value) {
|
|
576
|
+
if (!Array.isArray(value)) return [];
|
|
577
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function requiredString(value, name) {
|
|
581
|
+
const text = optionalString(value);
|
|
582
|
+
if (!text) throw rpcInvalidParams(`${name} is required`);
|
|
583
|
+
return text;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function optionalString(value) {
|
|
587
|
+
if (value == null) return "";
|
|
588
|
+
return String(value).trim();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function clampTimeout(value) {
|
|
592
|
+
const n = Number(value);
|
|
593
|
+
if (!Number.isFinite(n)) return BRIDGE_TIMEOUT_MS;
|
|
594
|
+
return Math.max(10_000, Math.min(900_000, Math.floor(n)));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function toolOk(data) {
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
600
|
+
structuredContent: data,
|
|
601
|
+
isError: false,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getPrompt(params) {
|
|
606
|
+
const name = String(params?.name || "");
|
|
607
|
+
const prompt = PROMPTS.find((entry) => entry.definition.name === name);
|
|
608
|
+
if (!prompt) throw rpcInvalidParams(`Unknown prompt: ${name}`);
|
|
609
|
+
return {
|
|
610
|
+
description: prompt.definition.description,
|
|
611
|
+
messages: [
|
|
612
|
+
{
|
|
613
|
+
role: "user",
|
|
614
|
+
content: { type: "text", text: prompt.text },
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function writeResult(id, result) {
|
|
621
|
+
writeRpc({ jsonrpc: "2.0", id, result });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function writeError(id, code, message, data = undefined) {
|
|
625
|
+
writeRpc({ jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function writeRpc(message) {
|
|
629
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function log(message) {
|
|
633
|
+
process.stderr.write(`[viveworker-mcp] ${message}\n`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function rpcInvalidParams(message) {
|
|
637
|
+
const error = new Error(message);
|
|
638
|
+
error.rpcCode = -32602;
|
|
639
|
+
return error;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function formatBridgeError(body) {
|
|
643
|
+
if (!body || typeof body !== "object") return "unknown error";
|
|
644
|
+
if (body.error?.message) return body.error.message;
|
|
645
|
+
if (body.error) return String(body.error);
|
|
646
|
+
return JSON.stringify(body).slice(0, 500);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function readOptionalFile(filePath) {
|
|
650
|
+
try {
|
|
651
|
+
return await fs.readFile(filePath, "utf8");
|
|
652
|
+
} catch {
|
|
653
|
+
return "";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function readPackageVersion() {
|
|
658
|
+
try {
|
|
659
|
+
const pkg = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
660
|
+
return String(pkg.version || "0.0.0");
|
|
661
|
+
} catch {
|
|
662
|
+
return "0.0.0";
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function envValue(text, key) {
|
|
667
|
+
for (const line of String(text || "").split(/\r?\n/u)) {
|
|
668
|
+
const trimmed = line.trim();
|
|
669
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
670
|
+
const eq = trimmed.indexOf("=");
|
|
671
|
+
if (eq === -1) continue;
|
|
672
|
+
if (trimmed.slice(0, eq) !== key) continue;
|
|
673
|
+
return unquoteEnvValue(trimmed.slice(eq + 1).trim());
|
|
674
|
+
}
|
|
675
|
+
return "";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function unquoteEnvValue(value) {
|
|
679
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
680
|
+
return value.slice(1, -1);
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isPlainObject(value) {
|
|
686
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function printConfigSnippets() {
|
|
690
|
+
const snippet = {
|
|
691
|
+
mcpServers: {
|
|
692
|
+
viveworker: {
|
|
693
|
+
command: "npx",
|
|
694
|
+
args: ["viveworker", "mcp"],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
console.log("Automatic install:");
|
|
699
|
+
console.log(" npx viveworker enable mcp --target claude");
|
|
700
|
+
console.log(" npx viveworker enable mcp --target cursor");
|
|
701
|
+
console.log(" npx viveworker enable mcp --target codex");
|
|
702
|
+
console.log("");
|
|
703
|
+
console.log("Claude Code:");
|
|
704
|
+
console.log(" claude mcp add --scope user viveworker -- npx viveworker mcp");
|
|
705
|
+
console.log("");
|
|
706
|
+
console.log("Claude Desktop / Cursor / Codex MCP config:");
|
|
707
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function printHelp() {
|
|
711
|
+
console.log("Usage:");
|
|
712
|
+
console.log(" viveworker mcp Start the stdio MCP server");
|
|
713
|
+
console.log(" viveworker mcp config Print MCP client config snippets");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const TOOLS = [
|
|
717
|
+
{
|
|
718
|
+
name: "viveworker_status",
|
|
719
|
+
title: "viveworker status",
|
|
720
|
+
description: "Return bridge, pairing, remote connection, A2A, and File Share status.",
|
|
721
|
+
inputSchema: { type: "object", additionalProperties: false },
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "viveworker_notify",
|
|
725
|
+
title: "Notify phone",
|
|
726
|
+
description: "Send an informational notification to the paired phone and leave it in the timeline.",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
type: "object",
|
|
729
|
+
additionalProperties: false,
|
|
730
|
+
properties: {
|
|
731
|
+
title: { type: "string" },
|
|
732
|
+
message: { type: "string" },
|
|
733
|
+
threadLabel: { type: "string" },
|
|
734
|
+
},
|
|
735
|
+
required: ["title", "message"],
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: "viveworker_ask",
|
|
740
|
+
title: "Ask on phone",
|
|
741
|
+
description: "Ask the paired phone a question and wait for the user's answer.",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
type: "object",
|
|
744
|
+
additionalProperties: false,
|
|
745
|
+
properties: {
|
|
746
|
+
title: { type: "string" },
|
|
747
|
+
question: { type: "string" },
|
|
748
|
+
options: {
|
|
749
|
+
type: "array",
|
|
750
|
+
items: {
|
|
751
|
+
anyOf: [
|
|
752
|
+
{ type: "string" },
|
|
753
|
+
{
|
|
754
|
+
type: "object",
|
|
755
|
+
additionalProperties: false,
|
|
756
|
+
properties: {
|
|
757
|
+
label: { type: "string" },
|
|
758
|
+
description: { type: "string" },
|
|
759
|
+
},
|
|
760
|
+
required: ["label"],
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
allowFreeform: { type: "boolean" },
|
|
766
|
+
timeoutMs: { type: "number" },
|
|
767
|
+
},
|
|
768
|
+
required: ["question"],
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "viveworker_request_approval",
|
|
773
|
+
title: "Request approval",
|
|
774
|
+
description: "Ask the paired phone to approve or reject a proposed action. This tool does not execute the action.",
|
|
775
|
+
inputSchema: {
|
|
776
|
+
type: "object",
|
|
777
|
+
additionalProperties: false,
|
|
778
|
+
properties: {
|
|
779
|
+
title: { type: "string" },
|
|
780
|
+
message: { type: "string" },
|
|
781
|
+
approvalKind: { type: "string" },
|
|
782
|
+
fileRefs: { type: "array", items: { type: "string" } },
|
|
783
|
+
diffText: { type: "string" },
|
|
784
|
+
timeoutMs: { type: "number" },
|
|
785
|
+
},
|
|
786
|
+
required: ["title", "message"],
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "viveworker_share_file",
|
|
791
|
+
title: "Share file",
|
|
792
|
+
description: "After phone approval, upload a workspace file to viveworker File Share and return the limited URL.",
|
|
793
|
+
inputSchema: {
|
|
794
|
+
type: "object",
|
|
795
|
+
additionalProperties: false,
|
|
796
|
+
properties: {
|
|
797
|
+
path: { type: "string" },
|
|
798
|
+
workspaceRoot: { type: "string" },
|
|
799
|
+
password: { type: "string" },
|
|
800
|
+
expiresDays: { type: "string" },
|
|
801
|
+
timeoutMs: { type: "number" },
|
|
802
|
+
},
|
|
803
|
+
required: ["path"],
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: "viveworker_thread_share",
|
|
808
|
+
title: "Thread share",
|
|
809
|
+
description: "Create a phone-approved Thread Share request to Codex, Claude, or the viveworker inbox.",
|
|
810
|
+
inputSchema: {
|
|
811
|
+
type: "object",
|
|
812
|
+
additionalProperties: false,
|
|
813
|
+
properties: {
|
|
814
|
+
content: { type: "string" },
|
|
815
|
+
targetConversationId: { type: "string" },
|
|
816
|
+
targetTool: { type: "string" },
|
|
817
|
+
targetCwd: { type: "string" },
|
|
818
|
+
sourceLabel: { type: "string" },
|
|
819
|
+
shareType: { type: "string" },
|
|
820
|
+
contextFiles: { type: "array", items: { type: "string" } },
|
|
821
|
+
},
|
|
822
|
+
required: ["content"],
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: "viveworker_send_a2a_task",
|
|
827
|
+
title: "Send A2A task",
|
|
828
|
+
description: "After phone approval, send a task to a registered A2A target alias from ~/.viveworker/a2a-targets.json.",
|
|
829
|
+
inputSchema: {
|
|
830
|
+
type: "object",
|
|
831
|
+
additionalProperties: false,
|
|
832
|
+
properties: {
|
|
833
|
+
target: { type: "string" },
|
|
834
|
+
instruction: { type: "string" },
|
|
835
|
+
metadata: { type: "object" },
|
|
836
|
+
timeoutMs: { type: "number" },
|
|
837
|
+
},
|
|
838
|
+
required: ["target", "instruction"],
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
const PROMPTS = [
|
|
844
|
+
{
|
|
845
|
+
definition: {
|
|
846
|
+
name: "use_viveworker_control_plane",
|
|
847
|
+
title: "Use viveworker control plane",
|
|
848
|
+
description: "Instruct the model to route human approvals, questions, sharing, and delegation through viveworker when useful.",
|
|
849
|
+
},
|
|
850
|
+
text: "Use viveworker as the mobile control plane when work needs human confirmation, a quick question, a file-share handoff, thread sharing, or A2A delegation. Prefer asking or requesting approval over guessing for irreversible, external, or sensitive actions.",
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
definition: {
|
|
854
|
+
name: "setup_viveworker",
|
|
855
|
+
title: "Set up viveworker",
|
|
856
|
+
description: "Guide the user through first-run setup, phone pairing, notification permission, and a smoke test.",
|
|
857
|
+
},
|
|
858
|
+
text: "When the user asks to set up viveworker, first call viveworker_status if available. If setup is missing, ask for confirmation before running or recommending `npx viveworker setup`. If no phone is trusted, guide the user to run `npx viveworker pair` and open the pairing URL on the phone. After pairing, re-check status and send a smoke notification with viveworker_notify.",
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
definition: {
|
|
862
|
+
name: "ask_before_risky_action",
|
|
863
|
+
title: "Ask before risky action",
|
|
864
|
+
description: "Ask the phone before proceeding with a risky or externally visible action.",
|
|
865
|
+
},
|
|
866
|
+
text: "Before you perform an action that is externally visible, hard to undo, or could expose local content, call viveworker_request_approval with a concise summary of the action and the concrete risk.",
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
definition: {
|
|
870
|
+
name: "share_deliverable",
|
|
871
|
+
title: "Share deliverable",
|
|
872
|
+
description: "Package a local deliverable through viveworker File Share with phone approval.",
|
|
873
|
+
},
|
|
874
|
+
text: "When the user asks to share a report, prototype, screenshot, CSV, or standalone HTML deliverable, use viveworker_share_file. Only share files inside the workspace and never share secrets or credentials.",
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
definition: {
|
|
878
|
+
name: "delegate_with_a2a",
|
|
879
|
+
title: "Delegate with A2A",
|
|
880
|
+
description: "Delegate suitable work to a registered A2A target through viveworker.",
|
|
881
|
+
},
|
|
882
|
+
text: "When another registered agent is a better fit for a bounded task, call viveworker_send_a2a_task with the target alias and a self-contained instruction. Keep the task scoped and include acceptance criteria.",
|
|
883
|
+
},
|
|
884
|
+
];
|
|
885
|
+
|
|
886
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
887
|
+
runMcpCli(process.argv.slice(2)).catch((error) => {
|
|
888
|
+
console.error(error.message || String(error));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
});
|
|
891
|
+
}
|