viveworker 0.8.9 → 0.9.0
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 +14 -3
- package/package.json +1 -1
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +7 -1
- package/scripts/lib/immutable-json-patch.mjs +142 -0
- package/scripts/lib/ipc-frame-worker.mjs +30 -0
- package/scripts/lib/ipc-message-projector.mjs +221 -0
- package/scripts/lib/length-prefixed-frame-decoder.mjs +128 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +24 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +10 -0
- package/scripts/lib/work-items.mjs +179 -0
- package/scripts/mcp-server.mjs +240 -16
- package/scripts/moltbook-api.mjs +27 -0
- package/scripts/moltbook-cli.mjs +12 -4
- package/scripts/moltbook-scout-auto.sh +105 -37
- package/scripts/viveworker-bridge.mjs +483 -234
- package/scripts/viveworker-claude-hook.mjs +5 -1
- package/scripts/viveworker.mjs +5 -0
- package/skills/viveworker-control-plane/SKILL.md +7 -0
- package/templates/CLAUDE.viveworker.md +12 -1
- package/web/app.css +106 -0
- package/web/app.js +183 -28
- package/web/build-id.js +1 -1
- package/web/i18n.js +44 -10
- package/web/remote-pairing/api-router.js +356 -11
- package/web/remote-pairing/transport.js +7 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const WORK_ITEM_SCHEMA_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
4
|
+
const STATE_PRIORITY = new Map([
|
|
5
|
+
["in_progress", 1],
|
|
6
|
+
["needs_action", 2],
|
|
7
|
+
["completed", 3],
|
|
8
|
+
["failed", 3],
|
|
9
|
+
["canceled", 3],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function cleanText(value) {
|
|
13
|
+
return String(value ?? "").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeProvider(value) {
|
|
17
|
+
const provider = cleanText(value).toLowerCase();
|
|
18
|
+
if (["claude", "moltbook", "a2a", "viveworker", "mcp"].includes(provider)) {
|
|
19
|
+
return provider;
|
|
20
|
+
}
|
|
21
|
+
return "codex";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function workItemCategory(item) {
|
|
25
|
+
switch (cleanText(item?.kind)) {
|
|
26
|
+
case "approval":
|
|
27
|
+
return cleanText(item?.approvalKind).includes("payment") ? "payment" : "approval";
|
|
28
|
+
case "plan":
|
|
29
|
+
case "plan_ready":
|
|
30
|
+
return "plan";
|
|
31
|
+
case "choice":
|
|
32
|
+
return "question";
|
|
33
|
+
case "moltbook_draft":
|
|
34
|
+
return "draft";
|
|
35
|
+
case "moltbook_reply":
|
|
36
|
+
return "reply";
|
|
37
|
+
case "thread_share":
|
|
38
|
+
return "handoff";
|
|
39
|
+
case "a2a_task":
|
|
40
|
+
return "delegation";
|
|
41
|
+
case "a2a_task_result":
|
|
42
|
+
return "delegation_result";
|
|
43
|
+
case "activity_status":
|
|
44
|
+
return "agent_run";
|
|
45
|
+
case "completion":
|
|
46
|
+
case "assistant_final":
|
|
47
|
+
return "result";
|
|
48
|
+
default:
|
|
49
|
+
return "work";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function terminalState(item) {
|
|
54
|
+
const taskStatus = cleanText(item?.taskStatus).toLowerCase();
|
|
55
|
+
if (["failed", "error"].includes(taskStatus)) {
|
|
56
|
+
return "failed";
|
|
57
|
+
}
|
|
58
|
+
if (["canceled", "cancelled", "rejected"].includes(taskStatus)) {
|
|
59
|
+
return "canceled";
|
|
60
|
+
}
|
|
61
|
+
return "completed";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function workItemPriority(item, state) {
|
|
65
|
+
if (workItemCategory(item) === "payment") {
|
|
66
|
+
return "urgent";
|
|
67
|
+
}
|
|
68
|
+
if (state === "needs_action") {
|
|
69
|
+
return cleanText(item?.kind) === "thread_share" ? "normal" : "high";
|
|
70
|
+
}
|
|
71
|
+
return state === "in_progress" ? "normal" : "low";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function workItemId(item) {
|
|
75
|
+
const provider = normalizeProvider(item?.provider);
|
|
76
|
+
const kind = cleanText(item?.kind) || "item";
|
|
77
|
+
const token = cleanText(item?.token);
|
|
78
|
+
const fallback = [item?.threadId, item?.createdAtMs, item?.title]
|
|
79
|
+
.map(cleanText)
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(":");
|
|
82
|
+
return `${provider}:${kind}:${token || fallback || "unknown"}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function workItemThreadKey(item) {
|
|
86
|
+
const threadId = cleanText(item?.threadId);
|
|
87
|
+
return threadId ? `${normalizeProvider(item?.provider)}:${threadId}` : "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function enrichWorkItem(item, state) {
|
|
91
|
+
const normalizedState = TERMINAL_STATES.has(state) ? state : cleanText(state) || "needs_action";
|
|
92
|
+
const requiresAction = normalizedState === "needs_action" && item?.supported !== false;
|
|
93
|
+
return {
|
|
94
|
+
...item,
|
|
95
|
+
provider: normalizeProvider(item?.provider),
|
|
96
|
+
work: {
|
|
97
|
+
schemaVersion: WORK_ITEM_SCHEMA_VERSION,
|
|
98
|
+
id: workItemId(item),
|
|
99
|
+
state: normalizedState,
|
|
100
|
+
category: workItemCategory(item),
|
|
101
|
+
priority: workItemPriority(item, normalizedState),
|
|
102
|
+
requiresAction,
|
|
103
|
+
sourceRef: {
|
|
104
|
+
provider: normalizeProvider(item?.provider),
|
|
105
|
+
kind: cleanText(item?.kind),
|
|
106
|
+
token: cleanText(item?.token),
|
|
107
|
+
threadId: cleanText(item?.threadId),
|
|
108
|
+
},
|
|
109
|
+
occurredAtMs: Math.max(0, Number(item?.createdAtMs) || 0),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compareWorkItems(left, right) {
|
|
115
|
+
const priorityRank = { urgent: 3, high: 2, normal: 1, low: 0 };
|
|
116
|
+
const priorityDelta = (priorityRank[right?.work?.priority] ?? 0) - (priorityRank[left?.work?.priority] ?? 0);
|
|
117
|
+
if (priorityDelta !== 0) {
|
|
118
|
+
return priorityDelta;
|
|
119
|
+
}
|
|
120
|
+
return Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeBucket(items, state) {
|
|
124
|
+
return (Array.isArray(items) ? items : [])
|
|
125
|
+
.filter((item) => item && typeof item === "object")
|
|
126
|
+
.map((item) => enrichWorkItem(item, state));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dedupeAcrossStates(items) {
|
|
130
|
+
const byId = new Map();
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
const id = cleanText(item?.work?.id);
|
|
133
|
+
if (!id) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const previous = byId.get(id);
|
|
137
|
+
if (!previous) {
|
|
138
|
+
byId.set(id, item);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const previousRank = STATE_PRIORITY.get(previous.work.state) ?? 0;
|
|
142
|
+
const nextRank = STATE_PRIORITY.get(item.work.state) ?? 0;
|
|
143
|
+
if (nextRank > previousRank || (nextRank === previousRank && Number(item.createdAtMs) > Number(previous.createdAtMs))) {
|
|
144
|
+
byId.set(id, item);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...byId.values()];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildWorkItemResponse({ pending = [], inProgress = [], completed = [], generatedAtMs = Date.now() } = {}) {
|
|
151
|
+
const pendingItems = normalizeBucket(pending, "needs_action");
|
|
152
|
+
const blockedThreadKeys = new Set(pendingItems.map(workItemThreadKey).filter(Boolean));
|
|
153
|
+
const activeItems = normalizeBucket(inProgress, "in_progress")
|
|
154
|
+
.filter((item) => !blockedThreadKeys.has(workItemThreadKey(item)));
|
|
155
|
+
const completedItems = (Array.isArray(completed) ? completed : [])
|
|
156
|
+
.filter((item) => item && typeof item === "object")
|
|
157
|
+
.map((item) => enrichWorkItem(item, terminalState(item)));
|
|
158
|
+
|
|
159
|
+
const deduped = dedupeAcrossStates([...activeItems, ...pendingItems, ...completedItems]);
|
|
160
|
+
const needsAction = deduped.filter((item) => item.work.state === "needs_action").sort(compareWorkItems);
|
|
161
|
+
const active = deduped.filter((item) => item.work.state === "in_progress").sort(compareWorkItems);
|
|
162
|
+
const terminal = deduped.filter((item) => TERMINAL_STATES.has(item.work.state)).sort(compareWorkItems);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
schemaVersion: WORK_ITEM_SCHEMA_VERSION,
|
|
166
|
+
generatedAtMs: Math.max(0, Number(generatedAtMs) || Date.now()),
|
|
167
|
+
counts: {
|
|
168
|
+
needsAction: needsAction.length,
|
|
169
|
+
inProgress: active.length,
|
|
170
|
+
completed: terminal.filter((item) => item.work.state === "completed").length,
|
|
171
|
+
failed: terminal.filter((item) => item.work.state === "failed").length,
|
|
172
|
+
},
|
|
173
|
+
pending: needsAction,
|
|
174
|
+
inProgress: active,
|
|
175
|
+
completed: terminal,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { WORK_ITEM_SCHEMA_VERSION };
|
package/scripts/mcp-server.mjs
CHANGED
|
@@ -127,7 +127,7 @@ class ViveworkerMcpServer {
|
|
|
127
127
|
websiteUrl: "https://viveworker.com",
|
|
128
128
|
},
|
|
129
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.",
|
|
130
|
+
"Use viveworker tools when you need to inspect viveworker state, notify, ask, request approval, share a file, hand off context, or delegate an A2A task through the user's paired mobile control plane.",
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -138,6 +138,20 @@ async function callTool(params) {
|
|
|
138
138
|
switch (name) {
|
|
139
139
|
case "viveworker_status":
|
|
140
140
|
return toolOk(await bridgeEvent({ eventType: "status" }, SHORT_TIMEOUT_MS));
|
|
141
|
+
case "viveworker_stats":
|
|
142
|
+
return toolOk(await toolStats(args));
|
|
143
|
+
case "viveworker_share_list":
|
|
144
|
+
return toolOk(await toolShareList(args));
|
|
145
|
+
case "viveworker_a2a_activity":
|
|
146
|
+
return toolOk(await toolA2AActivity(args));
|
|
147
|
+
case "viveworker_a2a_card":
|
|
148
|
+
return toolOk(await toolA2ACard(args));
|
|
149
|
+
case "viveworker_moltbook_list":
|
|
150
|
+
return toolOk(await toolMoltbookList(args));
|
|
151
|
+
case "viveworker_moltbook_show":
|
|
152
|
+
return toolOk(await toolMoltbookShow(args));
|
|
153
|
+
case "viveworker_moltbook_thread":
|
|
154
|
+
return toolOk(await toolMoltbookThread(args));
|
|
141
155
|
case "viveworker_notify":
|
|
142
156
|
return toolOk(await toolNotify(args));
|
|
143
157
|
case "viveworker_ask":
|
|
@@ -146,6 +160,8 @@ async function callTool(params) {
|
|
|
146
160
|
return toolOk(await toolRequestApproval(args));
|
|
147
161
|
case "viveworker_share_file":
|
|
148
162
|
return toolOk(await toolShareFile(args));
|
|
163
|
+
case "viveworker_share_replace":
|
|
164
|
+
return toolOk(await toolShareReplace(args));
|
|
149
165
|
case "viveworker_share_link":
|
|
150
166
|
return toolOk(await toolShareLink(args));
|
|
151
167
|
case "viveworker_thread_share":
|
|
@@ -157,6 +173,52 @@ async function callTool(params) {
|
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
175
|
|
|
176
|
+
async function toolStats(args) {
|
|
177
|
+
const pkg = optionalString(args.pkg || args.packageName);
|
|
178
|
+
const cliArgs = ["stats", "--json"];
|
|
179
|
+
if (pkg) {
|
|
180
|
+
assertSafeNpmPackageName(pkg);
|
|
181
|
+
cliArgs.push("--pkg", pkg);
|
|
182
|
+
}
|
|
183
|
+
return runViveworkerCliJson(cliArgs, 60_000);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function toolShareList(args) {
|
|
187
|
+
const cliArgs = ["share", "list", "--json"];
|
|
188
|
+
if (optionalBoolean(args.metrics)) {
|
|
189
|
+
cliArgs.push("--metrics");
|
|
190
|
+
}
|
|
191
|
+
return runViveworkerCliJson(cliArgs, 45_000);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function toolA2AActivity(_args) {
|
|
195
|
+
return runViveworkerCliJson(["a2a", "activity"], 30_000);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function toolA2ACard(_args) {
|
|
199
|
+
const output = await runViveworkerCliText(["a2a", "card"], 30_000);
|
|
200
|
+
return { ok: true, output: output.stdout.trim() };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function toolMoltbookList(args) {
|
|
204
|
+
const cliArgs = ["moltbook", "list"];
|
|
205
|
+
if (optionalBoolean(args.all)) {
|
|
206
|
+
cliArgs.push("--all");
|
|
207
|
+
}
|
|
208
|
+
const output = await runViveworkerCliText(cliArgs, 30_000);
|
|
209
|
+
return { ok: true, output: output.stdout.trim() };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function toolMoltbookShow(args) {
|
|
213
|
+
const commentId = requiredSafeIdentifier(args.commentId, "commentId");
|
|
214
|
+
return runViveworkerCliJson(["moltbook", "show", commentId], 30_000);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function toolMoltbookThread(args) {
|
|
218
|
+
const commentId = requiredSafeIdentifier(args.commentId, "commentId");
|
|
219
|
+
return runViveworkerCliJson(["moltbook", "thread", commentId], 45_000);
|
|
220
|
+
}
|
|
221
|
+
|
|
160
222
|
async function toolNotify(args) {
|
|
161
223
|
const title = requiredString(args.title, "title");
|
|
162
224
|
const message = requiredString(args.message, "message");
|
|
@@ -245,11 +307,46 @@ async function toolShareFile(args) {
|
|
|
245
307
|
return { approved: true, share: upload, tokenizedLink: link };
|
|
246
308
|
}
|
|
247
309
|
|
|
248
|
-
async function
|
|
249
|
-
const slug =
|
|
250
|
-
|
|
251
|
-
|
|
310
|
+
async function toolShareReplace(args) {
|
|
311
|
+
const slug = requiredShareSlug(args.slug);
|
|
312
|
+
const requestedPath = requiredString(args.path, "path");
|
|
313
|
+
const workspaceRoot = optionalString(args.workspaceRoot) || process.env.VIVEWORKER_MCP_WORKSPACE_ROOT || process.cwd();
|
|
314
|
+
const file = await validateSharePath(requestedPath, workspaceRoot, "share_replace");
|
|
315
|
+
const stat = await fs.stat(file.realPath);
|
|
316
|
+
const expiresDays = optionalString(args.expiresDays ?? args["expires-days"]);
|
|
317
|
+
const noOptimize = optionalBoolean(args.noOptimize ?? args["no-optimize"]);
|
|
318
|
+
const approval = await bridgeEvent({
|
|
319
|
+
eventType: "approval_request",
|
|
320
|
+
title: "Replace viveworker File Share file",
|
|
321
|
+
message: [
|
|
322
|
+
"An MCP client wants to replace the file behind an existing viveworker File Share URL.",
|
|
323
|
+
"",
|
|
324
|
+
`Share slug: ${slug}`,
|
|
325
|
+
`New file: ${file.displayPath}`,
|
|
326
|
+
`Size: ${stat.size} bytes`,
|
|
327
|
+
expiresDays ? `Expires days: ${expiresDays}` : "Expires days: keep existing/default",
|
|
328
|
+
noOptimize ? "HTML optimization: disabled for this replace" : "",
|
|
329
|
+
"",
|
|
330
|
+
"This changes what existing recipients see at the same share link.",
|
|
331
|
+
"Approve only if this replacement file is safe to send outside this Mac.",
|
|
332
|
+
].filter(Boolean).join("\n"),
|
|
333
|
+
approvalKind: "file_share_replace",
|
|
334
|
+
fileRefs: [file.displayPath],
|
|
335
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
336
|
+
}, clampTimeout(args.timeoutMs));
|
|
337
|
+
if (!approval.approved) {
|
|
338
|
+
return { approved: false, decision: approval.decision || "rejected" };
|
|
252
339
|
}
|
|
340
|
+
|
|
341
|
+
const replaceArgs = ["share", "replace", slug, file.realPath, "--json"];
|
|
342
|
+
if (expiresDays) replaceArgs.push("--expires-days", expiresDays);
|
|
343
|
+
if (noOptimize) replaceArgs.push("--no-optimize");
|
|
344
|
+
const replace = await runViveworkerCliJson(replaceArgs, 90_000);
|
|
345
|
+
return { approved: true, share: replace };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function toolShareLink(args) {
|
|
349
|
+
const slug = requiredShareSlug(args.slug);
|
|
253
350
|
const password = requiredString(args.password, "password");
|
|
254
351
|
if (password.length > 256) {
|
|
255
352
|
throw rpcInvalidParams("password is too long (max 256 characters)");
|
|
@@ -424,7 +521,7 @@ function httpJson(endpoint, options) {
|
|
|
424
521
|
});
|
|
425
522
|
}
|
|
426
523
|
|
|
427
|
-
async function validateSharePath(filePath, workspaceRoot) {
|
|
524
|
+
async function validateSharePath(filePath, workspaceRoot, toolName = "share_file") {
|
|
428
525
|
const root = path.resolve(String(workspaceRoot || ""));
|
|
429
526
|
const candidate = path.resolve(String(filePath || ""));
|
|
430
527
|
const [rootReal, fileReal] = await Promise.all([
|
|
@@ -433,16 +530,16 @@ async function validateSharePath(filePath, workspaceRoot) {
|
|
|
433
530
|
]);
|
|
434
531
|
const rel = path.relative(rootReal, fileReal);
|
|
435
532
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
436
|
-
throw new Error(
|
|
533
|
+
throw new Error(`${toolName} is limited to the current workspace root`);
|
|
437
534
|
}
|
|
438
535
|
const stat = await fs.stat(fileReal);
|
|
439
536
|
if (!stat.isFile()) {
|
|
440
|
-
throw new Error(
|
|
537
|
+
throw new Error(`${toolName} path must be a regular file`);
|
|
441
538
|
}
|
|
442
539
|
assertNotSensitivePath(rel || path.basename(fileReal));
|
|
443
540
|
const ext = path.extname(fileReal).toLowerCase();
|
|
444
541
|
if (!SHARE_FILE_EXTENSIONS.has(ext)) {
|
|
445
|
-
throw new Error(
|
|
542
|
+
throw new Error(`${toolName} accepts only ${Array.from(SHARE_FILE_EXTENSIONS).join(" / ")} files. Got: ${ext || "(no extension)"}`);
|
|
446
543
|
}
|
|
447
544
|
return {
|
|
448
545
|
realPath: fileReal,
|
|
@@ -476,7 +573,16 @@ function assertNotSensitivePath(relPath) {
|
|
|
476
573
|
}
|
|
477
574
|
}
|
|
478
575
|
|
|
479
|
-
function runViveworkerCliJson(args, timeoutMs) {
|
|
576
|
+
async function runViveworkerCliJson(args, timeoutMs) {
|
|
577
|
+
const { stdout } = await runViveworkerCliText(args, timeoutMs);
|
|
578
|
+
try {
|
|
579
|
+
return JSON.parse(stdout);
|
|
580
|
+
} catch {
|
|
581
|
+
throw new Error(`viveworker CLI returned non-JSON: ${stdout.slice(0, 200)}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function runViveworkerCliText(args, timeoutMs) {
|
|
480
586
|
return new Promise((resolve, reject) => {
|
|
481
587
|
const child = spawn(process.execPath, [viveworkerCli, ...args], {
|
|
482
588
|
cwd: packageRoot,
|
|
@@ -502,11 +608,7 @@ function runViveworkerCliJson(args, timeoutMs) {
|
|
|
502
608
|
reject(new Error((stderr || stdout || `viveworker CLI failed with code ${code}`).trim()));
|
|
503
609
|
return;
|
|
504
610
|
}
|
|
505
|
-
|
|
506
|
-
resolve(JSON.parse(stdout));
|
|
507
|
-
} catch {
|
|
508
|
-
reject(new Error(`viveworker CLI returned non-JSON: ${stdout.slice(0, 200)}`));
|
|
509
|
-
}
|
|
611
|
+
resolve({ stdout, stderr });
|
|
510
612
|
});
|
|
511
613
|
});
|
|
512
614
|
}
|
|
@@ -657,6 +759,29 @@ function slugFromShareUrl(value) {
|
|
|
657
759
|
}
|
|
658
760
|
}
|
|
659
761
|
|
|
762
|
+
function assertSafeNpmPackageName(value) {
|
|
763
|
+
const text = optionalString(value);
|
|
764
|
+
if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(text)) {
|
|
765
|
+
throw rpcInvalidParams("pkg must be a valid npm package name");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function requiredSafeIdentifier(value, name) {
|
|
770
|
+
const text = requiredString(value, name);
|
|
771
|
+
if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(text)) {
|
|
772
|
+
throw rpcInvalidParams(`${name} contains unsupported characters`);
|
|
773
|
+
}
|
|
774
|
+
return text;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function requiredShareSlug(value) {
|
|
778
|
+
const slug = requiredString(value, "slug");
|
|
779
|
+
if (!/^[A-Za-z0-9]+$/.test(slug)) {
|
|
780
|
+
throw rpcInvalidParams("slug must contain only letters and numbers");
|
|
781
|
+
}
|
|
782
|
+
return slug;
|
|
783
|
+
}
|
|
784
|
+
|
|
660
785
|
async function createShareTokenLink(slug, password, ttlHours) {
|
|
661
786
|
const linkArgs = ["share", "link", slug, "--password", password, "--json"];
|
|
662
787
|
if (ttlHours !== undefined) {
|
|
@@ -808,6 +933,87 @@ const TOOLS = [
|
|
|
808
933
|
description: "Return bridge, pairing, remote connection, A2A, and File Share status.",
|
|
809
934
|
inputSchema: { type: "object", additionalProperties: false },
|
|
810
935
|
},
|
|
936
|
+
{
|
|
937
|
+
name: "viveworker_stats",
|
|
938
|
+
title: "Read viveworker stats",
|
|
939
|
+
description: "Read viveworker package adoption and usage stats. Read-only.",
|
|
940
|
+
inputSchema: {
|
|
941
|
+
type: "object",
|
|
942
|
+
additionalProperties: false,
|
|
943
|
+
properties: {
|
|
944
|
+
pkg: { type: "string" },
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
annotations: { readOnlyHint: true },
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "viveworker_share_list",
|
|
951
|
+
title: "List File Share uploads",
|
|
952
|
+
description: "List File Share uploads and optionally include usage metrics. Read-only.",
|
|
953
|
+
inputSchema: {
|
|
954
|
+
type: "object",
|
|
955
|
+
additionalProperties: false,
|
|
956
|
+
properties: {
|
|
957
|
+
metrics: { type: "boolean" },
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
annotations: { readOnlyHint: true },
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
name: "viveworker_a2a_activity",
|
|
964
|
+
title: "Read A2A activity",
|
|
965
|
+
description: "Read local A2A activity from the default viveworker state. Read-only.",
|
|
966
|
+
inputSchema: { type: "object", additionalProperties: false },
|
|
967
|
+
annotations: { readOnlyHint: true },
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: "viveworker_a2a_card",
|
|
971
|
+
title: "Read A2A card",
|
|
972
|
+
description: "Read the current local A2A agent card settings. Read-only.",
|
|
973
|
+
inputSchema: { type: "object", additionalProperties: false },
|
|
974
|
+
annotations: { readOnlyHint: true },
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: "viveworker_moltbook_list",
|
|
978
|
+
title: "List Moltbook inbox",
|
|
979
|
+
description: "List Moltbook inbox comments. Read-only.",
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: "object",
|
|
982
|
+
additionalProperties: false,
|
|
983
|
+
properties: {
|
|
984
|
+
all: { type: "boolean" },
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
annotations: { readOnlyHint: true },
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "viveworker_moltbook_show",
|
|
991
|
+
title: "Read Moltbook comment",
|
|
992
|
+
description: "Show one Moltbook comment by commentId. Read-only.",
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: "object",
|
|
995
|
+
additionalProperties: false,
|
|
996
|
+
properties: {
|
|
997
|
+
commentId: { type: "string" },
|
|
998
|
+
},
|
|
999
|
+
required: ["commentId"],
|
|
1000
|
+
},
|
|
1001
|
+
annotations: { readOnlyHint: true },
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: "viveworker_moltbook_thread",
|
|
1005
|
+
title: "Read Moltbook thread",
|
|
1006
|
+
description: "Show the Moltbook thread for one commentId. Read-only.",
|
|
1007
|
+
inputSchema: {
|
|
1008
|
+
type: "object",
|
|
1009
|
+
additionalProperties: false,
|
|
1010
|
+
properties: {
|
|
1011
|
+
commentId: { type: "string" },
|
|
1012
|
+
},
|
|
1013
|
+
required: ["commentId"],
|
|
1014
|
+
},
|
|
1015
|
+
annotations: { readOnlyHint: true },
|
|
1016
|
+
},
|
|
811
1017
|
{
|
|
812
1018
|
name: "viveworker_notify",
|
|
813
1019
|
title: "Notify phone",
|
|
@@ -893,6 +1099,24 @@ const TOOLS = [
|
|
|
893
1099
|
required: ["path"],
|
|
894
1100
|
},
|
|
895
1101
|
},
|
|
1102
|
+
{
|
|
1103
|
+
name: "viveworker_share_replace",
|
|
1104
|
+
title: "Replace File Share file",
|
|
1105
|
+
description: "After phone approval, replace the file behind an existing viveworker File Share slug.",
|
|
1106
|
+
inputSchema: {
|
|
1107
|
+
type: "object",
|
|
1108
|
+
additionalProperties: false,
|
|
1109
|
+
properties: {
|
|
1110
|
+
slug: { type: "string" },
|
|
1111
|
+
path: { type: "string" },
|
|
1112
|
+
workspaceRoot: { type: "string" },
|
|
1113
|
+
expiresDays: { type: "string" },
|
|
1114
|
+
noOptimize: { type: "boolean" },
|
|
1115
|
+
timeoutMs: { type: "number" },
|
|
1116
|
+
},
|
|
1117
|
+
required: ["slug", "path"],
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
896
1120
|
{
|
|
897
1121
|
name: "viveworker_share_link",
|
|
898
1122
|
title: "Create File Share token URL",
|
|
@@ -977,7 +1201,7 @@ const PROMPTS = [
|
|
|
977
1201
|
title: "Share deliverable",
|
|
978
1202
|
description: "Package a local deliverable through viveworker File Share with phone approval.",
|
|
979
1203
|
},
|
|
980
|
-
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. If the user wants a password-protected share but the recipient should not know the password, set password plus tokenize=true, or use viveworker_share_link for an existing password-protected slug to mint a short-lived ?t= URL.",
|
|
1204
|
+
text: "When the user asks to share a report, prototype, screenshot, CSV, or standalone HTML deliverable, use viveworker_share_file. When the user asks to replace the file behind an existing File Share slug, use viveworker_share_replace. Only share files inside the workspace and never share secrets or credentials. If the user wants a password-protected share but the recipient should not know the password, set password plus tokenize=true, or use viveworker_share_link for an existing password-protected slug to mint a short-lived ?t= URL.",
|
|
981
1205
|
},
|
|
982
1206
|
{
|
|
983
1207
|
definition: {
|
package/scripts/moltbook-api.mjs
CHANGED
|
@@ -62,6 +62,33 @@ export const DEFAULT_INBOX_DIR = path.join(os.homedir(), ".viveworker", "moltboo
|
|
|
62
62
|
export const DEFAULT_SCOUT_STATE_FILE = path.join(os.homedir(), ".viveworker", "moltbook-scout-state.json");
|
|
63
63
|
export const DEFAULT_DRAFTS_DIR = path.join(os.homedir(), ".viveworker", "moltbook-drafts");
|
|
64
64
|
|
|
65
|
+
const PROVIDER_ERROR_TEXT_PATTERNS = [
|
|
66
|
+
/\bfailed to authenticate\b/iu,
|
|
67
|
+
/\binvalid authentication credentials\b/iu,
|
|
68
|
+
/\bauthentication_error\b/iu,
|
|
69
|
+
/\bapi error:\s*(?:401|403)\b/iu,
|
|
70
|
+
/\bincorrect api key\b/iu,
|
|
71
|
+
/\binvalid api key\b/iu,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export function looksLikeProviderErrorText(value) {
|
|
75
|
+
const text = String(value || "").trim();
|
|
76
|
+
if (!text) return false;
|
|
77
|
+
const compact = text.replace(/\s+/gu, " ");
|
|
78
|
+
if (PROVIDER_ERROR_TEXT_PATTERNS.some((pattern) => pattern.test(compact))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (!compact.startsWith("{")) return false;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(compact);
|
|
84
|
+
const type = String(parsed?.error?.type || parsed?.type || "").toLowerCase();
|
|
85
|
+
const message = String(parsed?.error?.message || parsed?.message || "").toLowerCase();
|
|
86
|
+
return type === "authentication_error" || message.includes("invalid authentication credentials");
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
export async function loadMoltbookEnv(envFile = DEFAULT_ENV_FILE) {
|
|
66
93
|
const env = {};
|
|
67
94
|
try {
|
package/scripts/moltbook-cli.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
pickInboxReplyCandidate,
|
|
37
37
|
reconcileInboxDraftMarkers,
|
|
38
38
|
getMoltbookReplyQuotaState,
|
|
39
|
+
looksLikeProviderErrorText,
|
|
39
40
|
loopbackFetch,
|
|
40
41
|
} from "./moltbook-api.mjs";
|
|
41
42
|
|
|
@@ -572,6 +573,9 @@ async function cmdPropose(postId, flags) {
|
|
|
572
573
|
if (!postId) fail("usage: viveworker moltbook propose <postId> --text \"...\"");
|
|
573
574
|
const text = typeof flags.text === "string" ? flags.text : "";
|
|
574
575
|
if (!text.trim()) fail("--text is required and must be non-empty");
|
|
576
|
+
if (looksLikeProviderErrorText(text)) {
|
|
577
|
+
fail("--text looks like an LLM/provider authentication error; refusing to create a Moltbook draft");
|
|
578
|
+
}
|
|
575
579
|
const ttlSec = Number(flags.ttl) || Number(flags.timeout) || 86400;
|
|
576
580
|
const parentCommentId = typeof flags["parent-id"] === "string" ? flags["parent-id"] : "";
|
|
577
581
|
const postTitle = typeof flags.title === "string" ? flags.title : "";
|
|
@@ -948,10 +952,6 @@ async function cmdCompose(flags) {
|
|
|
948
952
|
return;
|
|
949
953
|
}
|
|
950
954
|
|
|
951
|
-
// Mark slot as attempted (even before drafting, to avoid re-proposal on deny).
|
|
952
|
-
state.composeSlotsAttempted = [...attempted, slot];
|
|
953
|
-
await writeScoutState(state);
|
|
954
|
-
|
|
955
955
|
// Load persona.
|
|
956
956
|
let persona = "";
|
|
957
957
|
try {
|
|
@@ -1020,6 +1020,14 @@ async function cmdComposePropose(flags) {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
const token = submitRes?.token;
|
|
1022
1022
|
if (!token) fail(`bridge did not return a token: ${JSON.stringify(submitRes)}`);
|
|
1023
|
+
if (slot) {
|
|
1024
|
+
const state = rollScoutDayIfNeeded(await readScoutState());
|
|
1025
|
+
const attempted = Array.isArray(state.composeSlotsAttempted) ? state.composeSlotsAttempted : [];
|
|
1026
|
+
if (!attempted.includes(slot)) {
|
|
1027
|
+
state.composeSlotsAttempted = [...attempted, slot];
|
|
1028
|
+
await writeScoutState(state);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1023
1031
|
|
|
1024
1032
|
console.log(JSON.stringify({ ok: true, token, ttlSec, fireAndForget: true }));
|
|
1025
1033
|
}
|