viveworker 0.4.7 → 0.4.9
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/package.json +1 -1
- package/scripts/a2a-executor.mjs +267 -41
- package/scripts/a2a-handler.mjs +6 -1
- package/scripts/a2a-relay-client.mjs +13 -0
- package/scripts/moltbook-api.mjs +214 -5
- package/scripts/moltbook-cli.mjs +12 -397
- package/scripts/viveworker-bridge.mjs +471 -47
- package/web/app.css +249 -1
- package/web/app.js +307 -55
- package/web/i18n.js +69 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Local mobile companion for Codex Desktop and Claude Desktop — approvals, code review, Moltbook drafts, and A2A (Agent-to-Agent) task relay on your LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
package/scripts/a2a-executor.mjs
CHANGED
|
@@ -1,20 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* a2a-executor.mjs — Execute approved A2A tasks via Codex CLI.
|
|
2
|
+
* a2a-executor.mjs — Execute approved A2A tasks via Codex or Claude CLI.
|
|
3
3
|
*
|
|
4
4
|
* After the user approves an A2A task in the PWA, this module spawns
|
|
5
|
-
* `codex exec` to perform the work, captures the output,
|
|
6
|
-
* the task status to completed/failed.
|
|
5
|
+
* `codex exec` or `claude -p` to perform the work, captures the output,
|
|
6
|
+
* and updates the task status to completed/failed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import os from "node:os";
|
|
12
13
|
import { completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
13
14
|
|
|
15
|
+
const APP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// PATH augmentation for launchd environments
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build an augmented PATH that includes nvm, homebrew, and other common
|
|
23
|
+
* directories so spawned processes (claude, codex) can find `node`, etc.
|
|
24
|
+
*
|
|
25
|
+
* Under launchd the inherited PATH is minimal (/usr/bin:/bin:/usr/sbin:/sbin).
|
|
26
|
+
* We prepend well-known bin directories so child processes work correctly.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [binPath] - Resolved path to the binary being spawned;
|
|
29
|
+
* its parent directory is prepended to PATH.
|
|
30
|
+
*/
|
|
31
|
+
function buildAugmentedPath(binPath) {
|
|
32
|
+
const extra = [];
|
|
33
|
+
|
|
34
|
+
// Include the directory of the resolved binary itself
|
|
35
|
+
if (binPath && binPath !== "codex" && binPath !== "claude") {
|
|
36
|
+
extra.push(path.dirname(binPath));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// nvm — find the active or latest node version's bin directory
|
|
40
|
+
const nvmBase = path.join(os.homedir(), ".nvm", "versions", "node");
|
|
41
|
+
if (existsSync(nvmBase)) {
|
|
42
|
+
// Prefer NVM_BIN if set (interactive shell), otherwise scan versions
|
|
43
|
+
if (process.env.NVM_BIN && existsSync(process.env.NVM_BIN)) {
|
|
44
|
+
extra.push(process.env.NVM_BIN);
|
|
45
|
+
} else {
|
|
46
|
+
const ls = spawnSync("ls", [nvmBase], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
47
|
+
if (ls.status === 0 && ls.stdout) {
|
|
48
|
+
const versions = ls.stdout.trim().split("\n").filter(Boolean).reverse();
|
|
49
|
+
for (const v of versions) {
|
|
50
|
+
const binDir = path.join(nvmBase, v, "bin");
|
|
51
|
+
if (existsSync(path.join(binDir, "node"))) {
|
|
52
|
+
extra.push(binDir);
|
|
53
|
+
break; // use the latest version that has node
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Homebrew and common paths
|
|
61
|
+
for (const p of ["/opt/homebrew/bin", "/usr/local/bin"]) {
|
|
62
|
+
if (existsSync(p)) extra.push(p);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const basePath = process.env.PATH || "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
66
|
+
// Deduplicate while preserving order
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const parts = [];
|
|
69
|
+
for (const dir of [...extra, ...basePath.split(":")]) {
|
|
70
|
+
if (dir && !seen.has(dir)) {
|
|
71
|
+
seen.add(dir);
|
|
72
|
+
parts.push(dir);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return parts.join(":");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Try to find the claude binary by walking well-known locations.
|
|
80
|
+
* Returns the absolute path if found, or null.
|
|
81
|
+
*/
|
|
82
|
+
function findClaudeBin() {
|
|
83
|
+
// 1. `which` works if PATH is set (interactive shell, etc.)
|
|
84
|
+
const which = spawnSync("which", ["claude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
85
|
+
if (which.status === 0 && which.stdout) return which.stdout.trim();
|
|
86
|
+
|
|
87
|
+
// 2. ~/.claude/local/claude (official standalone install)
|
|
88
|
+
const standalone = path.join(os.homedir(), ".claude", "local", "claude");
|
|
89
|
+
if (existsSync(standalone)) return standalone;
|
|
90
|
+
|
|
91
|
+
// 3. nvm-managed global install — scan version dirs
|
|
92
|
+
const nvmBase = path.join(os.homedir(), ".nvm", "versions", "node");
|
|
93
|
+
if (existsSync(nvmBase)) {
|
|
94
|
+
const ls = spawnSync("ls", [nvmBase], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
95
|
+
if (ls.status === 0 && ls.stdout) {
|
|
96
|
+
const versions = ls.stdout.trim().split("\n").filter(Boolean).reverse();
|
|
97
|
+
for (const v of versions) {
|
|
98
|
+
const candidate = path.join(nvmBase, v, "bin", "claude");
|
|
99
|
+
if (existsSync(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Homebrew / common paths
|
|
105
|
+
for (const p of ["/usr/local/bin/claude", "/opt/homebrew/bin/claude"]) {
|
|
106
|
+
if (existsSync(p)) return p;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect which executor CLIs are available on this machine.
|
|
114
|
+
* Returns { codex: boolean, claude: boolean, claudeBin?: string, codexBin?: string }.
|
|
115
|
+
*/
|
|
116
|
+
export function detectAvailableExecutors() {
|
|
117
|
+
const codexWhich = spawnSync("which", ["codex"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
118
|
+
const codexBin = codexWhich.status === 0 && codexWhich.stdout
|
|
119
|
+
? codexWhich.stdout.trim()
|
|
120
|
+
: existsSync(APP_BUNDLE_PATH) ? APP_BUNDLE_PATH : null;
|
|
121
|
+
|
|
122
|
+
const claudeBin = findClaudeBin();
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
codex: !!codexBin,
|
|
126
|
+
claude: !!claudeBin,
|
|
127
|
+
codexBin: codexBin || undefined,
|
|
128
|
+
claudeBin: claudeBin || undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the codex binary path.
|
|
134
|
+
*/
|
|
135
|
+
function resolveCodexBin(config) {
|
|
136
|
+
if (config.codexBin) return config.codexBin;
|
|
137
|
+
const which = spawnSync("which", ["codex"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
138
|
+
if (which.status === 0 && which.stdout) return which.stdout.trim();
|
|
139
|
+
if (existsSync(APP_BUNDLE_PATH)) return APP_BUNDLE_PATH;
|
|
140
|
+
return "codex";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the claude binary path.
|
|
145
|
+
*/
|
|
146
|
+
function resolveClaudeBin() {
|
|
147
|
+
return findClaudeBin() || "claude";
|
|
148
|
+
}
|
|
149
|
+
|
|
14
150
|
const EXEC_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
15
151
|
|
|
16
152
|
/**
|
|
17
|
-
* Execute an A2A task by spawning
|
|
153
|
+
* Execute an A2A task by spawning the chosen executor.
|
|
18
154
|
*
|
|
19
155
|
* @param {object} task - A2A task object
|
|
20
156
|
* @param {object} config - Bridge config
|
|
@@ -23,54 +159,89 @@ const EXEC_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
|
23
159
|
* @param {object} helpers
|
|
24
160
|
* @param {Function} helpers.recordTimelineEntry
|
|
25
161
|
* @param {Function} helpers.saveState
|
|
162
|
+
* @param {Function} [helpers.recordHistoryItem] - Optional history-item recorder (adds to completed inbox)
|
|
163
|
+
* @param {Function} [helpers.deliverWebPushItem] - Optional web push delivery helper
|
|
164
|
+
* @param {string} [executor] - "codex" or "claude" (auto-detect if omitted)
|
|
26
165
|
*/
|
|
27
|
-
export async function executeA2ATask(task, config, runtime, state, { recordTimelineEntry, saveState }) {
|
|
166
|
+
export async function executeA2ATask(task, config, runtime, state, { recordTimelineEntry, recordHistoryItem, saveState, deliverWebPushItem }, executor) {
|
|
28
167
|
const instruction = task.instruction || "";
|
|
29
168
|
if (!instruction) {
|
|
30
169
|
failA2ATask(task, "No instruction provided");
|
|
31
170
|
return;
|
|
32
171
|
}
|
|
33
172
|
|
|
34
|
-
|
|
173
|
+
// Resolve executor if not specified.
|
|
174
|
+
if (!executor) {
|
|
175
|
+
const available = detectAvailableExecutors();
|
|
176
|
+
executor = available.codex ? "codex" : available.claude ? "claude" : "codex";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`[a2a-exec] Starting task ${task.id} via ${executor}: ${instruction.slice(0, 80)}`);
|
|
35
180
|
|
|
36
181
|
try {
|
|
37
|
-
const result =
|
|
182
|
+
const result = executor === "claude"
|
|
183
|
+
? await runClaudeExec(instruction)
|
|
184
|
+
: await runCodexExec(instruction, config);
|
|
38
185
|
completeA2ATask(task, result);
|
|
39
|
-
console.log(`[a2a-exec] Task ${task.id} completed (${result.length} chars)`);
|
|
186
|
+
console.log(`[a2a-exec] Task ${task.id} completed via ${executor} (${result.length} chars)`);
|
|
40
187
|
} catch (error) {
|
|
41
188
|
failA2ATask(task, error.message);
|
|
42
|
-
console.error(`[a2a-exec] Task ${task.id} failed: ${error.message}`);
|
|
189
|
+
console.error(`[a2a-exec] Task ${task.id} failed via ${executor}: ${error.message}`);
|
|
43
190
|
}
|
|
44
191
|
|
|
45
|
-
// Record completion in timeline
|
|
192
|
+
// Record completion in timeline AND history (history drives completed inbox list).
|
|
193
|
+
const resultEntry = {
|
|
194
|
+
stableId: `a2a_task_result:${task.id}`,
|
|
195
|
+
token: task.token,
|
|
196
|
+
kind: "a2a_task_result",
|
|
197
|
+
threadId: "a2a",
|
|
198
|
+
threadLabel: instruction.slice(0, 80),
|
|
199
|
+
title: task.status === "completed"
|
|
200
|
+
? `A2A ✅: ${instruction.slice(0, 60)}`
|
|
201
|
+
: `A2A ❌: ${instruction.slice(0, 60)}`,
|
|
202
|
+
summary: task.status === "completed"
|
|
203
|
+
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 160)
|
|
204
|
+
: task.statusMessage || "Failed",
|
|
205
|
+
instruction,
|
|
206
|
+
messageText: task.status === "completed"
|
|
207
|
+
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 500)
|
|
208
|
+
: task.statusMessage || "Failed",
|
|
209
|
+
taskStatus: task.status,
|
|
210
|
+
createdAtMs: task.updatedAtMs || Date.now(),
|
|
211
|
+
readOnly: true,
|
|
212
|
+
provider: "a2a",
|
|
213
|
+
};
|
|
46
214
|
try {
|
|
47
|
-
recordTimelineEntry({
|
|
48
|
-
|
|
49
|
-
runtime,
|
|
50
|
-
|
|
51
|
-
entry: {
|
|
52
|
-
stableId: `a2a_task_result:${task.id}`,
|
|
53
|
-
token: task.token,
|
|
54
|
-
kind: "a2a_task",
|
|
55
|
-
threadId: "a2a",
|
|
56
|
-
threadLabel: "A2A",
|
|
57
|
-
title: task.status === "completed"
|
|
58
|
-
? `A2A ✅: ${instruction.slice(0, 60)}`
|
|
59
|
-
: `A2A ❌: ${instruction.slice(0, 60)}`,
|
|
60
|
-
summary: task.statusMessage || "",
|
|
61
|
-
messageText: task.status === "completed"
|
|
62
|
-
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 500)
|
|
63
|
-
: task.statusMessage || "Failed",
|
|
64
|
-
taskStatus: task.status,
|
|
65
|
-
createdAtMs: task.updatedAtMs || Date.now(),
|
|
66
|
-
readOnly: true,
|
|
67
|
-
provider: "a2a",
|
|
68
|
-
},
|
|
69
|
-
});
|
|
215
|
+
recordTimelineEntry({ config, runtime, state, entry: resultEntry });
|
|
216
|
+
if (typeof recordHistoryItem === "function") {
|
|
217
|
+
recordHistoryItem({ config, runtime, state, item: resultEntry });
|
|
218
|
+
}
|
|
70
219
|
await saveState(config.stateFile, state);
|
|
71
220
|
} catch (error) {
|
|
72
221
|
console.error(`[a2a-exec-timeline] ${error.message}`);
|
|
73
222
|
}
|
|
223
|
+
|
|
224
|
+
// Send web push notification for completion/failure.
|
|
225
|
+
if (typeof deliverWebPushItem === "function") {
|
|
226
|
+
try {
|
|
227
|
+
const isCompleted = task.status === "completed";
|
|
228
|
+
const icon = isCompleted ? "✅" : "❌";
|
|
229
|
+
const resultBody = isCompleted
|
|
230
|
+
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 160)
|
|
231
|
+
: (task.statusMessage || "Failed").slice(0, 160);
|
|
232
|
+
await deliverWebPushItem({
|
|
233
|
+
config,
|
|
234
|
+
state,
|
|
235
|
+
kind: "a2a_task_result",
|
|
236
|
+
token: task.token,
|
|
237
|
+
stableId: `a2a_task_result:${task.id}`,
|
|
238
|
+
title: `A2A ${icon}: ${instruction.slice(0, 60)}`,
|
|
239
|
+
body: resultBody || instruction.slice(0, 160),
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`[a2a-exec-push] ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
74
245
|
}
|
|
75
246
|
|
|
76
247
|
/**
|
|
@@ -78,23 +249,28 @@ export async function executeA2ATask(task, config, runtime, state, { recordTimel
|
|
|
78
249
|
*/
|
|
79
250
|
function runCodexExec(instruction, config) {
|
|
80
251
|
return new Promise((resolve, reject) => {
|
|
81
|
-
const codexBin = config
|
|
82
|
-
const args = ["exec", "--
|
|
83
|
-
const cwd = config.workspaceRoot ||
|
|
252
|
+
const codexBin = resolveCodexBin(config);
|
|
253
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check", instruction];
|
|
254
|
+
const cwd = config.workspaceRoot || os.tmpdir();
|
|
84
255
|
|
|
85
256
|
const child = spawn(codexBin, args, {
|
|
86
257
|
cwd,
|
|
87
|
-
stdio: ["
|
|
88
|
-
env: {
|
|
258
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
259
|
+
env: {
|
|
260
|
+
...process.env,
|
|
261
|
+
PATH: buildAugmentedPath(codexBin),
|
|
262
|
+
CODEX_HOME: config.codexHome || path.join(os.homedir(), ".codex"),
|
|
263
|
+
},
|
|
89
264
|
timeout: EXEC_TIMEOUT_MS,
|
|
90
265
|
});
|
|
91
266
|
|
|
267
|
+
child.stdin.end();
|
|
268
|
+
|
|
92
269
|
let stdout = "";
|
|
93
270
|
let stderr = "";
|
|
94
271
|
|
|
95
272
|
child.stdout.on("data", (chunk) => {
|
|
96
273
|
stdout += chunk.toString("utf8");
|
|
97
|
-
// Cap output at 100KB
|
|
98
274
|
if (stdout.length > 100_000) {
|
|
99
275
|
child.kill("SIGTERM");
|
|
100
276
|
}
|
|
@@ -117,10 +293,60 @@ function runCodexExec(instruction, config) {
|
|
|
117
293
|
}
|
|
118
294
|
});
|
|
119
295
|
|
|
120
|
-
// Timeout fallback
|
|
121
296
|
setTimeout(() => {
|
|
122
297
|
try { child.kill("SIGTERM"); } catch { /* ignore */ }
|
|
123
298
|
reject(new Error("codex exec timed out (10 minutes)"));
|
|
124
299
|
}, EXEC_TIMEOUT_MS).unref?.();
|
|
125
300
|
});
|
|
126
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Spawn `claude -p` and capture stdout+stderr.
|
|
305
|
+
*/
|
|
306
|
+
function runClaudeExec(instruction) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const claudeBin = resolveClaudeBin();
|
|
309
|
+
const args = ["-p", instruction, "--output-format", "text"];
|
|
310
|
+
|
|
311
|
+
const child = spawn(claudeBin, args, {
|
|
312
|
+
cwd: os.tmpdir(),
|
|
313
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
314
|
+
env: { ...process.env, PATH: buildAugmentedPath(claudeBin) },
|
|
315
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
child.stdin.end();
|
|
319
|
+
|
|
320
|
+
let stdout = "";
|
|
321
|
+
let stderr = "";
|
|
322
|
+
|
|
323
|
+
child.stdout.on("data", (chunk) => {
|
|
324
|
+
stdout += chunk.toString("utf8");
|
|
325
|
+
if (stdout.length > 100_000) {
|
|
326
|
+
child.kill("SIGTERM");
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
child.stderr.on("data", (chunk) => {
|
|
330
|
+
stderr += chunk.toString("utf8");
|
|
331
|
+
if (stderr.length > 50_000) stderr = stderr.slice(-50_000);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
child.on("error", (error) => {
|
|
335
|
+
reject(new Error(`Failed to spawn claude: ${error.message}`));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
child.on("close", (code) => {
|
|
339
|
+
if (code === 0) {
|
|
340
|
+
resolve(stdout.trim() || "(completed with no output)");
|
|
341
|
+
} else {
|
|
342
|
+
const errorDetail = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
343
|
+
reject(new Error(`claude exec failed: ${errorDetail.slice(0, 500)}`));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
try { child.kill("SIGTERM"); } catch { /* ignore */ }
|
|
349
|
+
reject(new Error("claude exec timed out (10 minutes)"));
|
|
350
|
+
}, EXEC_TIMEOUT_MS).unref?.();
|
|
351
|
+
});
|
|
352
|
+
}
|
package/scripts/a2a-handler.mjs
CHANGED
|
@@ -291,6 +291,10 @@ async function handleMessageSend({
|
|
|
291
291
|
|
|
292
292
|
runtime.a2aTasksByToken.set(token, task);
|
|
293
293
|
|
|
294
|
+
// Update received task counter.
|
|
295
|
+
if (!state.a2aTaskStats) state.a2aTaskStats = { received: 0, completed: 0, denied: 0 };
|
|
296
|
+
state.a2aTaskStats.received += 1;
|
|
297
|
+
|
|
294
298
|
// Record timeline entry
|
|
295
299
|
try {
|
|
296
300
|
recordTimelineEntry({
|
|
@@ -302,9 +306,10 @@ async function handleMessageSend({
|
|
|
302
306
|
token,
|
|
303
307
|
kind: "a2a_task",
|
|
304
308
|
threadId: "a2a",
|
|
305
|
-
threadLabel:
|
|
309
|
+
threadLabel: cleanText(instruction).slice(0, 80),
|
|
306
310
|
title: `A2A: ${cleanText(instruction).slice(0, 80)}`,
|
|
307
311
|
summary: cleanText(instruction).slice(0, 160),
|
|
312
|
+
instruction,
|
|
308
313
|
messageText: instruction,
|
|
309
314
|
createdAtMs: now,
|
|
310
315
|
readOnly: false,
|
|
@@ -68,6 +68,7 @@ export async function registerWithRelay({ config, buildAgentCard }) {
|
|
|
68
68
|
userId,
|
|
69
69
|
bridgeSecret,
|
|
70
70
|
a2aApiKey: config.a2aApiKey || "",
|
|
71
|
+
acceptPublicTasks: config.a2aAcceptPublicTasks || false,
|
|
71
72
|
agentCard,
|
|
72
73
|
...(config.a2aRelayRegisterSecret ? { registerSecret: config.a2aRelayRegisterSecret } : {}),
|
|
73
74
|
};
|
|
@@ -166,6 +167,14 @@ export function stopRelayPolling() {
|
|
|
166
167
|
console.log("[a2a-relay] Polling stopped");
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Update the acceptPublicTasks flag on the relay via re-registration.
|
|
172
|
+
*/
|
|
173
|
+
export async function updatePublicTasksFlag({ config, buildAgentCard, acceptPublicTasks }) {
|
|
174
|
+
config.a2aAcceptPublicTasks = acceptPublicTasks;
|
|
175
|
+
return registerWithRelay({ config, buildAgentCard });
|
|
176
|
+
}
|
|
177
|
+
|
|
169
178
|
/**
|
|
170
179
|
* Return current relay connection status for the settings UI.
|
|
171
180
|
*/
|
|
@@ -261,6 +270,10 @@ async function ingestRelayTask({ relayTask, config, runtime, state, helpers }) {
|
|
|
261
270
|
|
|
262
271
|
runtime.a2aTasksByToken.set(token, task);
|
|
263
272
|
|
|
273
|
+
// Update received task counter.
|
|
274
|
+
if (!state.a2aTaskStats) state.a2aTaskStats = { received: 0, completed: 0, denied: 0 };
|
|
275
|
+
state.a2aTaskStats.received += 1;
|
|
276
|
+
|
|
264
277
|
// Record timeline entry (same as local A2A handler)
|
|
265
278
|
try {
|
|
266
279
|
recordTimelineEntry({
|
package/scripts/moltbook-api.mjs
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
7
8
|
|
|
8
9
|
export const API_BASE = "https://www.moltbook.com/api/v1";
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_ENV_FILE = path.join(os.homedir(), ".viveworker", "moltbook.env");
|
|
11
12
|
export const DEFAULT_INBOX_DIR = path.join(os.homedir(), ".viveworker", "moltbook-inbox");
|
|
12
13
|
export const DEFAULT_SCOUT_STATE_FILE = path.join(os.homedir(), ".viveworker", "moltbook-scout-state.json");
|
|
14
|
+
export const DEFAULT_DRAFTS_DIR = path.join(os.homedir(), ".viveworker", "moltbook-drafts");
|
|
13
15
|
|
|
14
16
|
export async function loadMoltbookEnv(envFile = DEFAULT_ENV_FILE) {
|
|
15
17
|
const env = {};
|
|
@@ -170,14 +172,14 @@ export function rollScoutDayIfNeeded(state) {
|
|
|
170
172
|
return state;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
export function recordComposeAttempt(state, title, postId) {
|
|
175
|
+
export function recordComposeAttempt(state, title, postId, type = "post") {
|
|
174
176
|
state.composedToday = (state.composedToday || 0) + 1;
|
|
175
177
|
state.lastComposeDay = todayKey();
|
|
176
178
|
if (!Array.isArray(state.recentComposeTitles)) state.recentComposeTitles = [];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
if (state.recentComposeTitles.length >
|
|
179
|
+
const entry = { title: String(title || ""), type };
|
|
180
|
+
if (postId) entry.postId = String(postId);
|
|
181
|
+
state.recentComposeTitles.unshift(entry);
|
|
182
|
+
if (state.recentComposeTitles.length > 30) state.recentComposeTitles.length = 30;
|
|
181
183
|
return state;
|
|
182
184
|
}
|
|
183
185
|
|
|
@@ -204,3 +206,210 @@ export async function listInboxItems(dir = DEFAULT_INBOX_DIR) {
|
|
|
204
206
|
return [];
|
|
205
207
|
}
|
|
206
208
|
}
|
|
209
|
+
|
|
210
|
+
// ---------- Draft persistence ----------
|
|
211
|
+
//
|
|
212
|
+
// Pending Moltbook drafts (reply proposals and original-post proposals) are
|
|
213
|
+
// written to disk so they survive bridge restarts. One JSON file per draft,
|
|
214
|
+
// keyed by its bridge-assigned token.
|
|
215
|
+
|
|
216
|
+
export async function ensureDraftsDir(dir = DEFAULT_DRAFTS_DIR) {
|
|
217
|
+
await fs.mkdir(dir, { recursive: true });
|
|
218
|
+
return dir;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function draftPathFor(token, dir = DEFAULT_DRAFTS_DIR) {
|
|
222
|
+
// Token may contain colons — encode for safe filenames.
|
|
223
|
+
const safe = encodeURIComponent(token);
|
|
224
|
+
return path.join(dir, `${safe}.json`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function writeDraft(draft, dir = DEFAULT_DRAFTS_DIR) {
|
|
228
|
+
await ensureDraftsDir(dir);
|
|
229
|
+
// Exclude runtime-only fields (decisionWaiters is an array of callbacks).
|
|
230
|
+
const { decisionWaiters, ...serializable } = draft;
|
|
231
|
+
await fs.writeFile(draftPathFor(draft.token, dir), JSON.stringify(serializable, null, 2) + "\n", "utf8");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function readDraft(token, dir = DEFAULT_DRAFTS_DIR) {
|
|
235
|
+
try {
|
|
236
|
+
const raw = await fs.readFile(draftPathFor(token, dir), "utf8");
|
|
237
|
+
return JSON.parse(raw);
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function deleteDraft(token, dir = DEFAULT_DRAFTS_DIR) {
|
|
244
|
+
try {
|
|
245
|
+
await fs.unlink(draftPathFor(token, dir));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.code !== "ENOENT") throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function listPendingDrafts(dir = DEFAULT_DRAFTS_DIR) {
|
|
252
|
+
try {
|
|
253
|
+
const files = await fs.readdir(dir);
|
|
254
|
+
const drafts = [];
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
if (!file.endsWith(".json")) continue;
|
|
257
|
+
try {
|
|
258
|
+
const raw = await fs.readFile(path.join(dir, file), "utf8");
|
|
259
|
+
const draft = JSON.parse(raw);
|
|
260
|
+
if (draft && !draft.decision) drafts.push(draft);
|
|
261
|
+
} catch {
|
|
262
|
+
// skip bad file
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return drafts;
|
|
266
|
+
} catch {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------- Verification puzzle solvers ----------
|
|
272
|
+
//
|
|
273
|
+
// Shared by the CLI (for manual `reply` flow) and the bridge (for fire-and-
|
|
274
|
+
// forget draft posting on approval).
|
|
275
|
+
|
|
276
|
+
// Naive verification-puzzle solver. Handles the obfuscated two-number
|
|
277
|
+
// arithmetic Moltbook currently uses (add / subtract / multiply). Returns
|
|
278
|
+
// `null` if it can't confidently solve — caller falls back to LLM or manual.
|
|
279
|
+
export function solveVerificationPuzzle(challengeText) {
|
|
280
|
+
if (!challengeText) return null;
|
|
281
|
+
const cleaned = String(challengeText)
|
|
282
|
+
.replace(/[^a-zA-Z0-9\s]/g, " ")
|
|
283
|
+
.toLowerCase();
|
|
284
|
+
const numberWords = {
|
|
285
|
+
zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9,
|
|
286
|
+
ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16,
|
|
287
|
+
seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, thirty: 30, forty: 40, fifty: 50,
|
|
288
|
+
sixty: 60, seventy: 70, eighty: 80, ninety: 90, hundred: 100,
|
|
289
|
+
};
|
|
290
|
+
const collapseRuns = (w) => w.replace(/([a-z])\1+/g, "$1");
|
|
291
|
+
const rawTokens = cleaned
|
|
292
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
293
|
+
.split(/\s+/)
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
const operationWords = new Set([
|
|
296
|
+
"total", "combined", "force", "velocity", "speed", "gains", "plus", "and",
|
|
297
|
+
"subtract", "minus", "less", "difference", "decreased", "loses", "lost", "slower", "slows", "slowed",
|
|
298
|
+
"multiply", "times", "product", "multiplied",
|
|
299
|
+
"divide", "divided", "ratio",
|
|
300
|
+
"how", "much", "what", "exerts", "new",
|
|
301
|
+
]);
|
|
302
|
+
const isKnown = (w) => numberWords[w] != null || numberWords[collapseRuns(w)] != null || operationWords.has(w) || operationWords.has(collapseRuns(w));
|
|
303
|
+
const merged = [];
|
|
304
|
+
let ti = 0;
|
|
305
|
+
while (ti < rawTokens.length) {
|
|
306
|
+
let best = rawTokens[ti];
|
|
307
|
+
let bestLen = 1;
|
|
308
|
+
let candidate = rawTokens[ti];
|
|
309
|
+
for (let span = 2; span <= Math.min(4, rawTokens.length - ti); span++) {
|
|
310
|
+
candidate += rawTokens[ti + span - 1];
|
|
311
|
+
if (isKnown(candidate) || isKnown(collapseRuns(candidate))) {
|
|
312
|
+
best = candidate;
|
|
313
|
+
bestLen = span;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
merged.push(best);
|
|
317
|
+
ti += bestLen;
|
|
318
|
+
}
|
|
319
|
+
const words = merged.map((w) => {
|
|
320
|
+
if (/^\d+$/.test(w)) return w;
|
|
321
|
+
if (numberWords[w] != null) return w;
|
|
322
|
+
const collapsed = collapseRuns(w);
|
|
323
|
+
if (numberWords[collapsed] != null) return collapsed;
|
|
324
|
+
return collapsed;
|
|
325
|
+
});
|
|
326
|
+
const numbers = [];
|
|
327
|
+
let i = 0;
|
|
328
|
+
while (i < words.length) {
|
|
329
|
+
const w = words[i];
|
|
330
|
+
if (/^\d+$/.test(w)) {
|
|
331
|
+
numbers.push(Number(w));
|
|
332
|
+
i += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (numberWords[w] != null) {
|
|
336
|
+
let total = numberWords[w];
|
|
337
|
+
i += 1;
|
|
338
|
+
while (i < words.length && numberWords[words[i]] != null) {
|
|
339
|
+
const next = numberWords[words[i]];
|
|
340
|
+
if (next === 100) total *= 100;
|
|
341
|
+
else if (next < 100 && total < 100) total += next;
|
|
342
|
+
else break;
|
|
343
|
+
i += 1;
|
|
344
|
+
}
|
|
345
|
+
numbers.push(total);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
i += 1;
|
|
349
|
+
}
|
|
350
|
+
if (numbers.length < 2) return null;
|
|
351
|
+
const a = numbers[0];
|
|
352
|
+
const b = numbers[1];
|
|
353
|
+
const hasWord = (w) => words.includes(w);
|
|
354
|
+
const hasAny = (...ws) => ws.some(hasWord);
|
|
355
|
+
let result;
|
|
356
|
+
if (hasAny("subtract", "minus", "less", "difference", "decreased", "loses", "lost", "slower", "slows", "slowed")) {
|
|
357
|
+
result = a - b;
|
|
358
|
+
} else if (hasAny("multiply", "times", "product", "multiplied")) {
|
|
359
|
+
result = a * b;
|
|
360
|
+
} else if (hasAny("divide", "divided", "ratio")) {
|
|
361
|
+
result = b !== 0 ? a / b : a;
|
|
362
|
+
} else {
|
|
363
|
+
result = a + b;
|
|
364
|
+
}
|
|
365
|
+
return result.toFixed(2);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// LLM-based verification puzzle solver. Shells out to claude or codex CLI.
|
|
369
|
+
// Returns the answer as "XX.XX" string, or null if unavailable.
|
|
370
|
+
export async function solvePuzzleWithLLM(challengeText) {
|
|
371
|
+
if (!challengeText) return null;
|
|
372
|
+
const prompt =
|
|
373
|
+
`The following text is an obfuscated arithmetic word problem from Moltbook (an AI social network). ` +
|
|
374
|
+
`The text has random capitalization, doubled letters, and stray punctuation — ignore all of that. ` +
|
|
375
|
+
`CRITICAL: ALL symbols (/, *, ^, ~, [, ], etc.) are NOISE, NOT arithmetic operators. ` +
|
|
376
|
+
`The operation is ALWAYS expressed in natural language words only. ` +
|
|
377
|
+
`Extract the numbers (written as words like "thirty five" = 35), determine the operation from WORDS ONLY ` +
|
|
378
|
+
`(addition: "total", "combined", "and", "plus", "gains", "new velocity"; ` +
|
|
379
|
+
`subtraction: "difference", "minus", "less", "loses"; ` +
|
|
380
|
+
`multiplication: "times", "product", "multiplied"; ` +
|
|
381
|
+
`division: "divided by", "ratio", "per"). ` +
|
|
382
|
+
`If no operation word is found, default to addition. ` +
|
|
383
|
+
`Compute the result and output ONLY the number with exactly 2 decimal places (e.g. "58.00"). No other text.\n\n` +
|
|
384
|
+
`Puzzle: ${challengeText}`;
|
|
385
|
+
for (const cmd of ["claude", "codex"]) {
|
|
386
|
+
let bin;
|
|
387
|
+
try {
|
|
388
|
+
bin = await new Promise((resolve) => {
|
|
389
|
+
const p = spawn("command", ["-v", cmd], { shell: "/bin/bash", stdio: ["ignore", "pipe", "ignore"] });
|
|
390
|
+
let out = "";
|
|
391
|
+
p.stdout.on("data", (d) => (out += d.toString()));
|
|
392
|
+
p.on("exit", (code) => resolve(code === 0 ? out.trim() : ""));
|
|
393
|
+
p.on("error", () => resolve(""));
|
|
394
|
+
});
|
|
395
|
+
} catch { bin = ""; }
|
|
396
|
+
if (!bin) continue;
|
|
397
|
+
const args = cmd === "claude" ? ["-p", prompt, "--output-format", "text"] : ["exec", prompt];
|
|
398
|
+
try {
|
|
399
|
+
const result = await new Promise((resolve, reject) => {
|
|
400
|
+
const p = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], timeout: 30000 });
|
|
401
|
+
let out = "";
|
|
402
|
+
p.stdout.on("data", (d) => (out += d.toString()));
|
|
403
|
+
p.on("exit", (code) => (code === 0 ? resolve(out.trim()) : reject(new Error(`exit ${code}`))));
|
|
404
|
+
p.on("error", reject);
|
|
405
|
+
});
|
|
406
|
+
const match = result.match(/(\d+\.\d{2})/);
|
|
407
|
+
if (match) return match[1];
|
|
408
|
+
const intMatch = result.match(/^(\d+)$/m);
|
|
409
|
+
if (intMatch) return `${intMatch[1]}.00`;
|
|
410
|
+
} catch {
|
|
411
|
+
// try next
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|