viveworker 0.2.1 → 0.4.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 CHANGED
@@ -193,6 +193,60 @@ npx viveworker doctor
193
193
  - On some devices, local CA trust is still manual before HTTPS works reliably
194
194
  - Web Push supports approvals, plans, multiple-choice questions, and completions
195
195
 
196
+ ## Moltbook Integration
197
+
198
+ `viveworker` connects to [Moltbook](https://www.moltbook.com), a social network for AI agents. Once configured, your agent automatically maintains a presence on Moltbook — replying to other agents and sharing what it builds — with you approving everything from your phone.
199
+
200
+ ### What it does
201
+
202
+ - **Incoming replies**: detects when other agents comment on your posts and notifies your phone so you can draft a reply
203
+ - **Draft approval on phone**: reply drafts and original post drafts appear in `Tasks` and `Timeline`, where you can approve, deny, or edit them from your phone
204
+ - **Auto-scout replies**: every 2 minutes, scans the Moltbook feed, scores posts against your agent's persona (0–100), batches candidates over a 30-minute window, picks the best match, drafts a reply via LLM, and proposes it for your approval
205
+ - **Original post drafts**: based on your daily coding activity, composes new posts in your agent's voice and proposes them at natural intervals — morning (yesterday recap), noon (morning progress), and evening (full-day summary). Up to 3 per day; deny any slot you don't want
206
+
207
+ ### How it works
208
+
209
+ 1. Define your agent's persona in `~/.viveworker/moltbook-persona.md` — voice, expertise, interests, topics to avoid
210
+ 2. The system filters all content through this persona: only activities and posts that match your agent's expertise are surfaced
211
+ 3. The Moltbook watcher pushes incoming replies and draft proposals into `Tasks` and `Timeline`
212
+ 4. On your phone, you can approve, deny, or edit the draft body before sending
213
+ 5. The Moltbook CLI long-polls for that decision, then posts to Moltbook and solves the verification puzzle automatically
214
+
215
+ ### Setup
216
+
217
+ ```bash
218
+ # Install the Moltbook watcher and auto-scout alongside viveworker
219
+ npx viveworker setup \
220
+ --moltbook \
221
+ --moltbook-api-key your-api-key \
222
+ --moltbook-agent-id your-agent-id \
223
+ --moltbook-agent-name "your-agent-name" \
224
+ --auto-scout
225
+
226
+ # Describe your agent's voice and expertise
227
+ npx viveworker moltbook persona init
228
+
229
+ # After setup, use start/stop as usual
230
+ npx viveworker start
231
+ ```
232
+
233
+ `setup --moltbook` writes `~/.viveworker/moltbook.env` and installs the Moltbook watcher.
234
+ `--auto-scout` installs the scheduled scout job. After that, `npx viveworker start` is your normal restart command for the main app.
235
+
236
+ Open `Settings > Moltbook` in the phone app to see the current auto-scout posting quota, current batch, and recent compose status.
237
+
238
+ ### Key commands
239
+
240
+ - `npx viveworker moltbook list` — show pending comment notifications
241
+ - `npx viveworker moltbook poll` — manually refresh Moltbook notifications once
242
+ - `npx viveworker moltbook reconcile` — resolve inbox items that were already replied to elsewhere
243
+ - `npx viveworker moltbook scout` — manually pick a feed candidate
244
+ - `npx viveworker moltbook propose <postId> --text "..."` — submit a reply draft for phone approval
245
+ - `npx viveworker moltbook compose` — inspect today's activity for original-post material
246
+ - `npx viveworker moltbook compose-propose --title "..." --content "..."` — submit an original-post draft for phone approval
247
+ - `npx viveworker moltbook persona show` — view your agent's persona
248
+ - `npx viveworker setup --auto-scout-uninstall` — remove the scheduled auto-scout job
249
+
196
250
  ## Roadmap
197
251
 
198
252
  Planned next steps include:
@@ -200,3 +254,4 @@ Planned next steps include:
200
254
  - Windows support
201
255
  - ✅ ~~Android support~~ (Apr 1, 2026)
202
256
  - ✅ ~~image attachment support from mobile~~ (Mar 26, 2026)
257
+ - ✅ ~~Moltbook integration~~ (Apr 10, 2026)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.2.1",
4
- "description": "Local mobile companion for Codex Desktop and Claude Desktop approvals, questions, completions, and code review on your LAN.",
3
+ "version": "0.4.0",
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",
7
7
  "keywords": [
@@ -19,6 +19,10 @@
19
19
  "approvals",
20
20
  "questions",
21
21
  "code-review",
22
+ "moltbook",
23
+ "ai-agents",
24
+ "a2a",
25
+ "agent-to-agent",
22
26
  "lan",
23
27
  "companion-app",
24
28
  "vivecoding"
@@ -33,6 +37,16 @@
33
37
  "scripts/viveworker.mjs",
34
38
  "scripts/viveworker-bridge.mjs",
35
39
  "scripts/viveworker-claude-hook.mjs",
40
+ "scripts/moltbook-api.mjs",
41
+ "scripts/moltbook-cli.mjs",
42
+ "scripts/moltbook-watcher.mjs",
43
+ "scripts/moltbook-scout-run.sh",
44
+ "scripts/moltbook-scout-auto.sh",
45
+ "scripts/com.viveworker.moltbook-scout.plist.sample",
46
+ "scripts/a2a-cli.mjs",
47
+ "scripts/a2a-handler.mjs",
48
+ "scripts/a2a-executor.mjs",
49
+ "scripts/a2a-relay-client.mjs",
36
50
  "scripts/lib",
37
51
  "web",
38
52
  "README.md",
@@ -0,0 +1,189 @@
1
+ /**
2
+ * a2a-cli.mjs — CLI for viveworker A2A relay operations.
3
+ *
4
+ * Usage:
5
+ * viveworker a2a setup --user-id <id> [--relay-url <url>] [--timeout <seconds>]
6
+ *
7
+ * The `setup` command:
8
+ * 1. Creates a setup session on the relay
9
+ * 2. Opens the GitHub OAuth URL in the user's browser
10
+ * 3. Polls for completion (user approves in browser)
11
+ * 4. Writes credentials to ~/.viveworker/a2a.env
12
+ * 5. Bridge can then be restarted to auto-connect
13
+ */
14
+
15
+ import { promises as fs } from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import { execSync } from "node:child_process";
19
+ import { upsertEnvText } from "./lib/pairing.mjs";
20
+
21
+ const A2A_ENV_FILE = path.join(os.homedir(), ".viveworker", "a2a.env");
22
+ const DEFAULT_RELAY_URL = "https://a2a.viveworker.com";
23
+ const DEFAULT_TIMEOUT = 300; // 5 minutes
24
+ const POLL_INTERVAL = 3000; // 3 seconds
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // CLI entry point
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export async function runA2ACli(args) {
31
+ const cmd = args[0];
32
+
33
+ switch (cmd) {
34
+ case "setup":
35
+ return handleSetup(args.slice(1));
36
+ default:
37
+ console.log("Usage: viveworker a2a setup --user-id <id> [--relay-url <url>] [--timeout <seconds>]");
38
+ if (cmd && cmd !== "help" && cmd !== "--help") {
39
+ throw new Error(`Unknown command: ${cmd}`);
40
+ }
41
+ }
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // setup command
46
+ // ---------------------------------------------------------------------------
47
+
48
+ async function handleSetup(args) {
49
+ const flags = parseFlags(args);
50
+ const userId = flags["user-id"] || flags["userId"];
51
+ const relayUrl = (flags["relay-url"] || flags["relayUrl"] || DEFAULT_RELAY_URL).replace(/\/$/u, "");
52
+ const timeout = Number(flags["timeout"]) || DEFAULT_TIMEOUT;
53
+
54
+ if (!userId) {
55
+ throw new Error("--user-id is required\nUsage: viveworker a2a setup --user-id <id>");
56
+ }
57
+
58
+ console.log(`\n🔗 viveworker A2A Relay Setup`);
59
+ console.log(` Relay: ${relayUrl}`);
60
+ console.log(` User ID: ${userId}\n`);
61
+
62
+ // Step 1: Create setup session
63
+ console.log("⏳ Creating setup session...");
64
+ const sessionRes = await fetchJson(`${relayUrl}/auth/setup`, {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json" },
67
+ body: JSON.stringify({ userId }),
68
+ });
69
+
70
+ if (sessionRes.error) {
71
+ throw new Error(`Setup session failed: ${sessionRes.error}`);
72
+ }
73
+
74
+ const { token, authUrl } = sessionRes;
75
+ console.log(`✅ Session created\n`);
76
+
77
+ // Step 2: Open browser
78
+ console.log(`🌐 Opening browser for GitHub authorization...`);
79
+ console.log(` ${authUrl}\n`);
80
+ openBrowser(authUrl);
81
+
82
+ console.log(`⏳ Waiting for GitHub authorization (timeout: ${timeout}s)...`);
83
+
84
+ // Step 3: Poll for completion
85
+ const deadline = Date.now() + timeout * 1000;
86
+ let result = null;
87
+
88
+ while (Date.now() < deadline) {
89
+ await sleep(POLL_INTERVAL);
90
+
91
+ const pollRes = await fetchJson(`${relayUrl}/auth/setup/${token}`);
92
+
93
+ if (pollRes.status === "completed") {
94
+ result = pollRes.credentials;
95
+ break;
96
+ }
97
+
98
+ if (pollRes.status === "error") {
99
+ throw new Error(`Registration failed: ${pollRes.error}`);
100
+ }
101
+
102
+ if (pollRes.error) {
103
+ throw new Error(`Poll error: ${pollRes.error}`);
104
+ }
105
+
106
+ // Still pending — continue polling
107
+ }
108
+
109
+ if (!result) {
110
+ throw new Error("Timeout waiting for GitHub authorization. Please try again.");
111
+ }
112
+
113
+ console.log(`\n✅ GitHub authorization complete (@${result.githubLogin})\n`);
114
+
115
+ // Step 4: Write to a2a.env
116
+ console.log(`📝 Writing credentials to ${A2A_ENV_FILE}...`);
117
+
118
+ let currentEnv = "";
119
+ try {
120
+ currentEnv = await fs.readFile(A2A_ENV_FILE, "utf8");
121
+ } catch {
122
+ // File may not exist
123
+ }
124
+
125
+ const updated = upsertEnvText(currentEnv, {
126
+ A2A_API_KEY: result.a2aApiKey,
127
+ A2A_RELAY_URL: result.relayUrl,
128
+ A2A_RELAY_USER_ID: result.userId,
129
+ A2A_RELAY_REGISTER_SECRET: result.registerSecret,
130
+ });
131
+
132
+ await fs.mkdir(path.dirname(A2A_ENV_FILE), { recursive: true, mode: 0o700 });
133
+ await fs.writeFile(A2A_ENV_FILE, updated, { mode: 0o600 });
134
+
135
+ console.log(`✅ Credentials saved\n`);
136
+ console.log(`🚀 Setup complete! Restart your viveworker bridge to connect.`);
137
+ console.log(` Your A2A endpoint: ${result.relayUrl}/${result.userId}\n`);
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Helpers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function parseFlags(args) {
145
+ const flags = {};
146
+ for (let i = 0; i < args.length; i++) {
147
+ const arg = args[i];
148
+ if (arg.startsWith("--")) {
149
+ const eqIdx = arg.indexOf("=");
150
+ if (eqIdx !== -1) {
151
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
152
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
153
+ flags[arg.slice(2)] = args[++i];
154
+ } else {
155
+ flags[arg.slice(2)] = true;
156
+ }
157
+ }
158
+ }
159
+ return flags;
160
+ }
161
+
162
+ async function fetchJson(url, options = {}) {
163
+ const controller = new AbortController();
164
+ const timer = setTimeout(() => controller.abort(), 15000);
165
+ try {
166
+ const res = await fetch(url, { ...options, signal: controller.signal });
167
+ return await res.json();
168
+ } finally {
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+
173
+ function openBrowser(url) {
174
+ try {
175
+ if (process.platform === "darwin") {
176
+ execSync(`open "${url}"`, { stdio: "ignore" });
177
+ } else if (process.platform === "linux") {
178
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
179
+ } else if (process.platform === "win32") {
180
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
181
+ }
182
+ } catch {
183
+ // Browser open failed — URL is already printed for manual use
184
+ }
185
+ }
186
+
187
+ function sleep(ms) {
188
+ return new Promise((resolve) => setTimeout(resolve, ms));
189
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * a2a-executor.mjs — Execute approved A2A tasks via Codex CLI.
3
+ *
4
+ * After the user approves an A2A task in the PWA, this module spawns
5
+ * `codex exec` to perform the work, captures the output, and updates
6
+ * the task status to completed/failed.
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import { completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
13
+
14
+ const EXEC_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
15
+
16
+ /**
17
+ * Execute an A2A task by spawning `codex exec`.
18
+ *
19
+ * @param {object} task - A2A task object
20
+ * @param {object} config - Bridge config
21
+ * @param {object} runtime - Bridge runtime
22
+ * @param {object} state - Persistent state
23
+ * @param {object} helpers
24
+ * @param {Function} helpers.recordTimelineEntry
25
+ * @param {Function} helpers.saveState
26
+ */
27
+ export async function executeA2ATask(task, config, runtime, state, { recordTimelineEntry, saveState }) {
28
+ const instruction = task.instruction || "";
29
+ if (!instruction) {
30
+ failA2ATask(task, "No instruction provided");
31
+ return;
32
+ }
33
+
34
+ console.log(`[a2a-exec] Starting task ${task.id}: ${instruction.slice(0, 80)}`);
35
+
36
+ try {
37
+ const result = await runCodexExec(instruction, config);
38
+ completeA2ATask(task, result);
39
+ console.log(`[a2a-exec] Task ${task.id} completed (${result.length} chars)`);
40
+ } catch (error) {
41
+ failA2ATask(task, error.message);
42
+ console.error(`[a2a-exec] Task ${task.id} failed: ${error.message}`);
43
+ }
44
+
45
+ // Record completion in timeline
46
+ try {
47
+ recordTimelineEntry({
48
+ config,
49
+ runtime,
50
+ state,
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
+ });
70
+ await saveState(config.stateFile, state);
71
+ } catch (error) {
72
+ console.error(`[a2a-exec-timeline] ${error.message}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Spawn `codex exec` and capture stdout+stderr.
78
+ */
79
+ function runCodexExec(instruction, config) {
80
+ return new Promise((resolve, reject) => {
81
+ const codexBin = config.codexBin || "codex";
82
+ const args = ["exec", "--prompt", instruction, "--approval-mode", "full-auto"];
83
+ const cwd = config.workspaceRoot || process.cwd();
84
+
85
+ const child = spawn(codexBin, args, {
86
+ cwd,
87
+ stdio: ["ignore", "pipe", "pipe"],
88
+ env: { ...process.env, CODEX_HOME: config.codexHome || path.join(os.homedir(), ".codex") },
89
+ timeout: EXEC_TIMEOUT_MS,
90
+ });
91
+
92
+ let stdout = "";
93
+ let stderr = "";
94
+
95
+ child.stdout.on("data", (chunk) => {
96
+ stdout += chunk.toString("utf8");
97
+ // Cap output at 100KB
98
+ if (stdout.length > 100_000) {
99
+ child.kill("SIGTERM");
100
+ }
101
+ });
102
+ child.stderr.on("data", (chunk) => {
103
+ stderr += chunk.toString("utf8");
104
+ if (stderr.length > 50_000) stderr = stderr.slice(-50_000);
105
+ });
106
+
107
+ child.on("error", (error) => {
108
+ reject(new Error(`Failed to spawn codex: ${error.message}`));
109
+ });
110
+
111
+ child.on("close", (code) => {
112
+ if (code === 0) {
113
+ resolve(stdout.trim() || "(completed with no output)");
114
+ } else {
115
+ const errorDetail = stderr.trim() || stdout.trim() || `exit code ${code}`;
116
+ reject(new Error(`codex exec failed: ${errorDetail.slice(0, 500)}`));
117
+ }
118
+ });
119
+
120
+ // Timeout fallback
121
+ setTimeout(() => {
122
+ try { child.kill("SIGTERM"); } catch { /* ignore */ }
123
+ reject(new Error("codex exec timed out (10 minutes)"));
124
+ }, EXEC_TIMEOUT_MS).unref?.();
125
+ });
126
+ }