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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.4.0",
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",
@@ -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("Usage: viveworker a2a setup --user-id <id> [--relay-url <url>] [--timeout <seconds>]");
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}\n`);
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 updated = upsertEnvText(currentEnv, {
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
  // ---------------------------------------------------------------------------
@@ -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
- return {
22
- schemaVersion: "1.0",
23
- humanReadableId: "viveworker/viveworker",
24
- agentVersion: config.version || "0.3.0",
25
- name: "viveworker",
26
- description:
27
- "LAN-connected AI companion that bridges Codex and Claude Desktop. " +
28
- "Can execute coding tasks, file operations, and code reviews with human approval.",
29
- url: `${baseUrl.replace(/\/$/u, "")}/a2a`,
30
- provider: { name: "viveworker" },
31
- capabilities: {
32
- a2aVersion: "0.2.3",
33
- streaming: false,
34
- pushNotifications: false,
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 and auto-start relay
15198
+ // Hot-reload: detect a2a.env changes (new config or updated card)
15196
15199
  const now = Date.now();
15197
- if (!config.a2aRelayUrl && now - lastA2aEnvCheckAt > A2A_ENV_CHECK_INTERVAL_MS) {
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
- config.a2aRelayUrl = newRelayUrl;
15206
- config.a2aRelayUserId = newRelayUserId;
15207
- config.a2aRelayRegisterSecret = newRelayRegisterSecret;
15208
- config.a2aApiKey = cleanText(process.env.A2A_API_KEY || config.a2aApiKey || "");
15209
- console.log(`[a2a-relay] Detected new relay config, registering...`);
15210
- const regResult = await registerWithRelay({ config, buildAgentCard });
15211
- if (regResult.ok) {
15212
- startRelayPolling({
15213
- config,
15214
- runtime,
15215
- state,
15216
- helpers: { recordTimelineEntry, deliverWebPushItem, saveState, historyToken, cleanText },
15217
- });
15218
- } else {
15219
- console.error(`[a2a-relay] Registration failed: ${regResult.error}`);
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) {