viveworker 0.4.0 ā 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/a2a-cli.mjs +183 -4
- package/scripts/a2a-handler.mjs +35 -17
- package/scripts/viveworker-bridge.mjs +41 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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-cli.mjs
CHANGED
|
@@ -33,8 +33,15 @@ export async function runA2ACli(args) {
|
|
|
33
33
|
switch (cmd) {
|
|
34
34
|
case "setup":
|
|
35
35
|
return handleSetup(args.slice(1));
|
|
36
|
+
case "activity":
|
|
37
|
+
return handleActivity(args.slice(1));
|
|
38
|
+
case "card":
|
|
39
|
+
return handleCard(args.slice(1));
|
|
36
40
|
default:
|
|
37
|
-
console.log("
|
|
41
|
+
console.log("Commands:");
|
|
42
|
+
console.log(" viveworker a2a setup --user-id <id> [--description <text>] [--skills <csv>]");
|
|
43
|
+
console.log(" viveworker a2a activity [--state-file <path>]");
|
|
44
|
+
console.log(" viveworker a2a card [--description <text>] [--skills <csv>] [--avatar <url-or-emoji>]");
|
|
38
45
|
if (cmd && cmd !== "help" && cmd !== "--help") {
|
|
39
46
|
throw new Error(`Unknown command: ${cmd}`);
|
|
40
47
|
}
|
|
@@ -50,6 +57,9 @@ async function handleSetup(args) {
|
|
|
50
57
|
const userId = flags["user-id"] || flags["userId"];
|
|
51
58
|
const relayUrl = (flags["relay-url"] || flags["relayUrl"] || DEFAULT_RELAY_URL).replace(/\/$/u, "");
|
|
52
59
|
const timeout = Number(flags["timeout"]) || DEFAULT_TIMEOUT;
|
|
60
|
+
const description = flags["description"] || "";
|
|
61
|
+
const skillsRaw = flags["skills"] || "";
|
|
62
|
+
const avatar = flags["avatar"] || "";
|
|
53
63
|
|
|
54
64
|
if (!userId) {
|
|
55
65
|
throw new Error("--user-id is required\nUsage: viveworker a2a setup --user-id <id>");
|
|
@@ -57,7 +67,11 @@ async function handleSetup(args) {
|
|
|
57
67
|
|
|
58
68
|
console.log(`\nš viveworker A2A Relay Setup`);
|
|
59
69
|
console.log(` Relay: ${relayUrl}`);
|
|
60
|
-
console.log(` User ID: ${userId}
|
|
70
|
+
console.log(` User ID: ${userId}`);
|
|
71
|
+
if (description) console.log(` Desc: ${description}`);
|
|
72
|
+
if (skillsRaw) console.log(` Skills: ${skillsRaw}`);
|
|
73
|
+
if (avatar) console.log(` Avatar: ${avatar}`);
|
|
74
|
+
console.log();
|
|
61
75
|
|
|
62
76
|
// Step 1: Create setup session
|
|
63
77
|
console.log("ā³ Creating setup session...");
|
|
@@ -122,12 +136,17 @@ async function handleSetup(args) {
|
|
|
122
136
|
// File may not exist
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
const
|
|
139
|
+
const envVars = {
|
|
126
140
|
A2A_API_KEY: result.a2aApiKey,
|
|
127
141
|
A2A_RELAY_URL: result.relayUrl,
|
|
128
142
|
A2A_RELAY_USER_ID: result.userId,
|
|
129
143
|
A2A_RELAY_REGISTER_SECRET: result.registerSecret,
|
|
130
|
-
}
|
|
144
|
+
};
|
|
145
|
+
if (description) envVars.A2A_DESCRIPTION = description;
|
|
146
|
+
if (skillsRaw) envVars.A2A_SKILLS = skillsRaw;
|
|
147
|
+
if (avatar) envVars.A2A_AVATAR = avatar;
|
|
148
|
+
|
|
149
|
+
const updated = upsertEnvText(currentEnv, envVars);
|
|
131
150
|
|
|
132
151
|
await fs.mkdir(path.dirname(A2A_ENV_FILE), { recursive: true, mode: 0o700 });
|
|
133
152
|
await fs.writeFile(A2A_ENV_FILE, updated, { mode: 0o600 });
|
|
@@ -137,6 +156,166 @@ async function handleSetup(args) {
|
|
|
137
156
|
console.log(` Your A2A endpoint: ${result.relayUrl}/${result.userId}\n`);
|
|
138
157
|
}
|
|
139
158
|
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// activity command
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
async function handleActivity(args) {
|
|
164
|
+
const flags = parseFlags(args);
|
|
165
|
+
|
|
166
|
+
// Find state file: explicit flag > STATE_FILE env > default locations
|
|
167
|
+
const candidates = [
|
|
168
|
+
flags["state-file"] || flags["stateFile"],
|
|
169
|
+
process.env.STATE_FILE,
|
|
170
|
+
path.join(os.homedir(), ".viveworker", "state.json"),
|
|
171
|
+
path.join(process.cwd(), ".viveworker-state.json"),
|
|
172
|
+
].filter(Boolean);
|
|
173
|
+
|
|
174
|
+
let stateData = null;
|
|
175
|
+
let usedPath = "";
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = await fs.readFile(candidate, "utf8");
|
|
179
|
+
stateData = JSON.parse(raw);
|
|
180
|
+
usedPath = candidate;
|
|
181
|
+
break;
|
|
182
|
+
} catch {
|
|
183
|
+
// try next
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!stateData) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Could not find viveworker state file.\n" +
|
|
190
|
+
"Try: viveworker a2a activity --state-file /path/to/.viveworker-state.json\n" +
|
|
191
|
+
"Or set STATE_FILE environment variable."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const entries = stateData.recentTimelineEntries || [];
|
|
196
|
+
if (entries.length === 0) {
|
|
197
|
+
console.log(JSON.stringify({ totalEntries: 0, providers: {}, threads: [], recentTasks: [] }));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Provider usage
|
|
202
|
+
const providers = {};
|
|
203
|
+
for (const e of entries) {
|
|
204
|
+
if (e.provider) providers[e.provider] = (providers[e.provider] || 0) + 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Thread topics with activity counts
|
|
208
|
+
const threadMap = new Map();
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
const label = e.threadLabel || e.title;
|
|
211
|
+
if (!label || label === "Moltbook") continue;
|
|
212
|
+
if (!threadMap.has(label)) threadMap.set(label, { count: 0, providers: new Set() });
|
|
213
|
+
const t = threadMap.get(label);
|
|
214
|
+
t.count++;
|
|
215
|
+
if (e.provider) t.providers.add(e.provider);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const threads = [...threadMap.entries()]
|
|
219
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
220
|
+
.map(([label, v]) => ({
|
|
221
|
+
label: label.slice(0, 120),
|
|
222
|
+
count: v.count,
|
|
223
|
+
providers: [...v.providers],
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
// Recent task titles (from assistant_final and user_message kinds)
|
|
227
|
+
const taskKinds = new Set(["assistant_final", "user_message", "completion"]);
|
|
228
|
+
const recentTasks = [];
|
|
229
|
+
const seenTitles = new Set();
|
|
230
|
+
for (let i = entries.length - 1; i >= 0 && recentTasks.length < 15; i--) {
|
|
231
|
+
const e = entries[i];
|
|
232
|
+
if (!taskKinds.has(e.kind)) continue;
|
|
233
|
+
const title = (e.title || e.threadLabel || "").trim();
|
|
234
|
+
if (!title || seenTitles.has(title)) continue;
|
|
235
|
+
seenTitles.add(title);
|
|
236
|
+
recentTasks.push({
|
|
237
|
+
title: title.slice(0, 120),
|
|
238
|
+
provider: e.provider || "",
|
|
239
|
+
kind: e.kind,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = {
|
|
244
|
+
stateFile: usedPath,
|
|
245
|
+
totalEntries: entries.length,
|
|
246
|
+
providers,
|
|
247
|
+
threads,
|
|
248
|
+
recentTasks,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
console.log(JSON.stringify(result, null, 2));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// card command
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
async function handleCard(args) {
|
|
259
|
+
const flags = parseFlags(args);
|
|
260
|
+
const description = flags["description"] || "";
|
|
261
|
+
const skillsRaw = flags["skills"] || "";
|
|
262
|
+
const avatar = flags["avatar"] || "";
|
|
263
|
+
|
|
264
|
+
if (!description && !skillsRaw && !avatar) {
|
|
265
|
+
// No flags ā show current values
|
|
266
|
+
let currentEnv = "";
|
|
267
|
+
try {
|
|
268
|
+
currentEnv = await fs.readFile(A2A_ENV_FILE, "utf8");
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error(`No a2a.env found at ${A2A_ENV_FILE}. Run 'viveworker a2a setup' first.`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const currentDesc = envValue(currentEnv, "A2A_DESCRIPTION");
|
|
274
|
+
const currentSkills = envValue(currentEnv, "A2A_SKILLS");
|
|
275
|
+
const currentAvatar = envValue(currentEnv, "A2A_AVATAR");
|
|
276
|
+
|
|
277
|
+
console.log(`\nš Current Agent Card settings (${A2A_ENV_FILE})\n`);
|
|
278
|
+
console.log(` Description: ${currentDesc || "(not set)"}`);
|
|
279
|
+
console.log(` Skills: ${currentSkills || "(not set)"}`);
|
|
280
|
+
console.log(` Avatar: ${currentAvatar || "(not set ā uses GitHub avatar or š¤)"}\n`);
|
|
281
|
+
console.log(`To update: viveworker a2a card --description "..." --skills "..." --avatar "..."`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update a2a.env
|
|
286
|
+
let currentEnv = "";
|
|
287
|
+
try {
|
|
288
|
+
currentEnv = await fs.readFile(A2A_ENV_FILE, "utf8");
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error(`No a2a.env found at ${A2A_ENV_FILE}. Run 'viveworker a2a setup' first.`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const updates = {};
|
|
294
|
+
if (description) updates.A2A_DESCRIPTION = description;
|
|
295
|
+
if (skillsRaw) updates.A2A_SKILLS = skillsRaw;
|
|
296
|
+
if (avatar) updates.A2A_AVATAR = avatar;
|
|
297
|
+
|
|
298
|
+
const updated = upsertEnvText(currentEnv, updates);
|
|
299
|
+
await fs.writeFile(A2A_ENV_FILE, updated, { mode: 0o600 });
|
|
300
|
+
|
|
301
|
+
console.log(`\nā
Agent Card updated in ${A2A_ENV_FILE}\n`);
|
|
302
|
+
if (description) console.log(` Description: ${description}`);
|
|
303
|
+
if (skillsRaw) console.log(` Skills: ${skillsRaw}`);
|
|
304
|
+
if (avatar) console.log(` Avatar: ${avatar}`);
|
|
305
|
+
console.log(`\nš The bridge will pick up the change within 30 seconds and re-register with the relay.\n`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function envValue(text, key) {
|
|
309
|
+
for (const line of text.split(/\r?\n/)) {
|
|
310
|
+
const trimmed = line.trim();
|
|
311
|
+
if (trimmed.startsWith("#") || !trimmed) continue;
|
|
312
|
+
const eq = trimmed.indexOf("=");
|
|
313
|
+
if (eq === -1) continue;
|
|
314
|
+
if (trimmed.slice(0, eq) === key) return trimmed.slice(eq + 1);
|
|
315
|
+
}
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
|
|
140
319
|
// ---------------------------------------------------------------------------
|
|
141
320
|
// Helpers
|
|
142
321
|
// ---------------------------------------------------------------------------
|
package/scripts/a2a-handler.mjs
CHANGED
|
@@ -18,22 +18,22 @@ import crypto from "node:crypto";
|
|
|
18
18
|
|
|
19
19
|
export function buildAgentCard(config) {
|
|
20
20
|
const baseUrl = config.a2aPublicUrl || config.publicBaseUrl || `http://localhost:${config.port || 7860}`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
skills
|
|
21
|
+
|
|
22
|
+
// Custom description from A2A_DESCRIPTION env var (set during setup)
|
|
23
|
+
const description = config.a2aDescription ||
|
|
24
|
+
"LAN-connected AI companion that bridges Codex and Claude Desktop. " +
|
|
25
|
+
"Can execute coding tasks, file operations, and code reviews with human approval.";
|
|
26
|
+
|
|
27
|
+
// Custom skills from A2A_SKILLS env var (comma-separated tags ā skill objects)
|
|
28
|
+
let skills;
|
|
29
|
+
if (config.a2aSkills) {
|
|
30
|
+
skills = config.a2aSkills.split(",").map((s) => s.trim()).filter(Boolean).map((tag) => ({
|
|
31
|
+
id: tag,
|
|
32
|
+
name: tag,
|
|
33
|
+
description: tag,
|
|
34
|
+
}));
|
|
35
|
+
} else {
|
|
36
|
+
skills = [
|
|
37
37
|
{
|
|
38
38
|
id: "code-task",
|
|
39
39
|
name: "Code Task",
|
|
@@ -57,9 +57,27 @@ export function buildAgentCard(config) {
|
|
|
57
57
|
required: ["target"],
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
|
-
]
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const card = {
|
|
64
|
+
schemaVersion: "1.0",
|
|
65
|
+
humanReadableId: `viveworker/${config.a2aRelayUserId || "viveworker"}`,
|
|
66
|
+
agentVersion: config.version || "0.3.0",
|
|
67
|
+
name: "viveworker",
|
|
68
|
+
description,
|
|
69
|
+
url: `${baseUrl.replace(/\/$/u, "")}/a2a`,
|
|
70
|
+
provider: { name: "viveworker" },
|
|
71
|
+
capabilities: {
|
|
72
|
+
a2aVersion: "0.2.3",
|
|
73
|
+
streaming: false,
|
|
74
|
+
pushNotifications: false,
|
|
75
|
+
},
|
|
76
|
+
skills,
|
|
61
77
|
authSchemes: [{ scheme: "apiKey", in: "header", name: "X-A2A-Key" }],
|
|
62
78
|
};
|
|
79
|
+
if (config.a2aAvatar) card.avatar = config.a2aAvatar;
|
|
80
|
+
return card;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
// ---------------------------------------------------------------------------
|
|
@@ -13787,6 +13787,9 @@ function buildConfig(cli) {
|
|
|
13787
13787
|
moltbookApiKey: cleanText(process.env.MOLTBOOK_API_KEY || readMoltbookEnvKey() || ""),
|
|
13788
13788
|
a2aApiKey: cleanText(process.env.A2A_API_KEY || readA2AEnvKey() || ""),
|
|
13789
13789
|
a2aPublicUrl: cleanText(process.env.A2A_PUBLIC_URL || ""),
|
|
13790
|
+
a2aDescription: cleanText(process.env.A2A_DESCRIPTION || ""),
|
|
13791
|
+
a2aSkills: cleanText(process.env.A2A_SKILLS || ""),
|
|
13792
|
+
a2aAvatar: cleanText(process.env.A2A_AVATAR || ""),
|
|
13790
13793
|
a2aRelayUrl: cleanText(process.env.A2A_RELAY_URL || ""),
|
|
13791
13794
|
a2aRelayUserId: cleanText(process.env.A2A_RELAY_USER_ID || ""),
|
|
13792
13795
|
a2aRelaySecret: cleanText(process.env.A2A_RELAY_SECRET || ""),
|
|
@@ -15192,31 +15195,52 @@ async function main() {
|
|
|
15192
15195
|
console.error(`[scan-error] ${error.message}`);
|
|
15193
15196
|
}
|
|
15194
15197
|
|
|
15195
|
-
// Hot-reload: detect a2a.env changes
|
|
15198
|
+
// Hot-reload: detect a2a.env changes (new config or updated card)
|
|
15196
15199
|
const now = Date.now();
|
|
15197
|
-
if (
|
|
15200
|
+
if (now - lastA2aEnvCheckAt > A2A_ENV_CHECK_INTERVAL_MS) {
|
|
15198
15201
|
lastA2aEnvCheckAt = now;
|
|
15199
15202
|
try {
|
|
15200
15203
|
loadEnvFile(path.join(os.homedir(), ".viveworker", "a2a.env"));
|
|
15201
15204
|
const newRelayUrl = cleanText(process.env.A2A_RELAY_URL || "");
|
|
15202
15205
|
const newRelayUserId = cleanText(process.env.A2A_RELAY_USER_ID || "");
|
|
15203
15206
|
const newRelayRegisterSecret = cleanText(process.env.A2A_RELAY_REGISTER_SECRET || "");
|
|
15207
|
+
const newDescription = cleanText(process.env.A2A_DESCRIPTION || "");
|
|
15208
|
+
const newSkills = cleanText(process.env.A2A_SKILLS || "");
|
|
15209
|
+
const newAvatar = cleanText(process.env.A2A_AVATAR || "");
|
|
15210
|
+
|
|
15204
15211
|
if (newRelayUrl && newRelayUserId) {
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
config.
|
|
15208
|
-
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15212
|
+
// Check if this is a first-time connect or a card update
|
|
15213
|
+
const isNew = !config.a2aRelayUrl;
|
|
15214
|
+
const cardChanged = config.a2aDescription !== newDescription || config.a2aSkills !== newSkills || config.a2aAvatar !== newAvatar;
|
|
15215
|
+
|
|
15216
|
+
if (isNew || cardChanged) {
|
|
15217
|
+
config.a2aRelayUrl = newRelayUrl;
|
|
15218
|
+
config.a2aRelayUserId = newRelayUserId;
|
|
15219
|
+
config.a2aRelayRegisterSecret = newRelayRegisterSecret;
|
|
15220
|
+
config.a2aApiKey = cleanText(process.env.A2A_API_KEY || config.a2aApiKey || "");
|
|
15221
|
+
config.a2aDescription = newDescription;
|
|
15222
|
+
config.a2aSkills = newSkills;
|
|
15223
|
+
config.a2aAvatar = newAvatar;
|
|
15224
|
+
|
|
15225
|
+
if (isNew) {
|
|
15226
|
+
console.log(`[a2a-relay] Detected new relay config, registering...`);
|
|
15227
|
+
} else {
|
|
15228
|
+
console.log(`[a2a-relay] Agent Card changed, re-registering...`);
|
|
15229
|
+
}
|
|
15230
|
+
|
|
15231
|
+
const regResult = await registerWithRelay({ config, buildAgentCard });
|
|
15232
|
+
if (regResult.ok) {
|
|
15233
|
+
if (isNew) {
|
|
15234
|
+
startRelayPolling({
|
|
15235
|
+
config,
|
|
15236
|
+
runtime,
|
|
15237
|
+
state,
|
|
15238
|
+
helpers: { recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText },
|
|
15239
|
+
});
|
|
15240
|
+
}
|
|
15241
|
+
} else {
|
|
15242
|
+
console.error(`[a2a-relay] Registration failed: ${regResult.error}`);
|
|
15243
|
+
}
|
|
15220
15244
|
}
|
|
15221
15245
|
}
|
|
15222
15246
|
} catch (error) {
|