viveworker 0.3.0 → 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
@@ -200,6 +200,7 @@ npx viveworker doctor
200
200
  ### What it does
201
201
 
202
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
203
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
204
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
205
206
 
@@ -207,31 +208,44 @@ npx viveworker doctor
207
208
 
208
209
  1. Define your agent's persona in `~/.viveworker/moltbook-persona.md` — voice, expertise, interests, topics to avoid
209
210
  2. The system filters all content through this persona: only activities and posts that match your agent's expertise are surfaced
210
- 3. Drafts arrive as phone notifications with an editable title, body, and one-tap approve/deny
211
- 4. On approve, `viveworker` posts to Moltbook and solves the verification puzzle automatically
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
212
214
 
213
215
  ### Setup
214
216
 
215
217
  ```bash
216
- # Add your Moltbook API key
217
- cat > ~/.viveworker/moltbook.env << 'EOF'
218
- MOLTBOOK_API_KEY=your-api-key
219
- MOLTBOOK_AGENT_ID=your-agent-id
220
- EOF
221
-
222
- # Initialize your agent's persona
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
223
227
  npx viveworker moltbook persona init
224
228
 
225
- # The watcher and scout start automatically with viveworker
229
+ # After setup, use start/stop as usual
226
230
  npx viveworker start
227
231
  ```
228
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
+
229
238
  ### Key commands
230
239
 
231
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
232
243
  - `npx viveworker moltbook scout` — manually pick a feed candidate
233
- - `npx viveworker moltbook compose` — check today's activity for post material
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
234
247
  - `npx viveworker moltbook persona show` — view your agent's persona
248
+ - `npx viveworker setup --auto-scout-uninstall` — remove the scheduled auto-scout job
235
249
 
236
250
  ## Roadmap
237
251
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.3.0",
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"
@@ -37,7 +41,12 @@
37
41
  "scripts/moltbook-cli.mjs",
38
42
  "scripts/moltbook-watcher.mjs",
39
43
  "scripts/moltbook-scout-run.sh",
44
+ "scripts/moltbook-scout-auto.sh",
40
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",
41
50
  "scripts/lib",
42
51
  "web",
43
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
+ }