osborn 0.9.80 → 0.9.82

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.
@@ -0,0 +1,32 @@
1
+ # Voice-E2E — browser testing with ears, a mouth, hands and a brain
2
+
3
+ ## SKILL IDENTITY
4
+ Name: voice-e2e
5
+ Version: 1 (packaged seed — the served copy at the URL below is source of truth)
6
+ Served from: https://www.voice-native.com/api/test-skill
7
+ Landing page: https://www.voice-native.com/test-skill
8
+
9
+ Agent-driven end-to-end testing for voice and web apps in a REAL browser:
10
+ speak into the page's mic (synthesized on the fly), hear and transcribe what
11
+ it plays back, click via a natural-language brain, improvise conversations,
12
+ and get video + audio replays per run. Works on any website.
13
+
14
+ ## UPDATE CHECK (notify-first, run before major test/research runs)
15
+ This packaged copy ships with every machine and may lag the served version.
16
+ Check, then NOTIFY the user before updating — never silently force:
17
+
18
+ ```bash
19
+ SERVED_V=$(curl -sf --max-time 10 https://www.voice-native.com/api/test-skill | grep -m1 '^Version:' | awk '{print $2}')
20
+ LOCAL_V=$(grep -m1 '^Version:' ~/.claude/skills/voice-e2e/SKILL.md 2>/dev/null | awk '{print $2}')
21
+ [ -n "$SERVED_V" ] && [ "$SERVED_V" != "$LOCAL_V" ] && echo "UPDATE AVAILABLE: voice-e2e v${LOCAL_V:-none} -> v$SERVED_V — tell the user what's new and ask before updating (curl -sS https://www.voice-native.com/api/test-skill -o ~/.claude/skills/voice-e2e/SKILL.md), then refresh the harness bundle per the served INSTALLATION."
22
+ ```
23
+
24
+ If the user approves (or has pre-approved auto-updates), update the skill
25
+ file AND re-run the served INSTALLATION step 2 (harness bundle refresh) so
26
+ the accompanying files stay in lockstep with the skill.
27
+
28
+ ## FIRST USE
29
+ Fetch the served copy and follow its INSTALLATION section — it materializes
30
+ the harness (~/voice-e2e-harness/), installs dependencies, and asks the user
31
+ for a GOOGLE_API_KEY (required, Gemini brain) plus optional OPENAI/DEEPGRAM
32
+ keys for the voice mouth and audible verification.
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ function loadSkillsList(agentDir) {
70
70
  // Extract description from first paragraph after heading
71
71
  const descMatch = content.match(/^#[^\n]+\n+([^\n#]+)/m);
72
72
  const description = descMatch ? descMatch[1].trim() : '';
73
- skills.push({ name, description });
73
+ skills.push({ name, description, folder: skillName });
74
74
  }
75
75
  }
76
76
  }
@@ -1237,6 +1237,8 @@ async function main() {
1237
1237
  // Track state
1238
1238
  let pendingSessionClose = null; // Tracks async session close for reconnect safety
1239
1239
  let currentSession = null;
1240
+ // 0.9.81: callable handler reference — see registration site for why.
1241
+ let participantConnectedHandler = null;
1240
1242
  let currentAgent = null; // For updateChatCtx() context injection
1241
1243
  let currentLLM = null;
1242
1244
  // Agent-side "alone in room" leave timer (see Room-presence lifecycle note up
@@ -1384,8 +1386,8 @@ async function main() {
1384
1386
  if (Date.now() - last < 60_000)
1385
1387
  return;
1386
1388
  adoptAttempts.set(p.identity, Date.now());
1387
- console.log(`📥 [ADOPT-POLL] ${p.identity} present with no session — re-emitting ParticipantConnected`);
1388
- room.emit(RoomEvent.ParticipantConnected, p);
1389
+ console.log(`📥 [ADOPT-POLL] ${p.identity} present with no session — invoking ParticipantConnected handler directly`);
1390
+ participantConnectedHandler?.(p).catch((e) => console.error('📥 [ADOPT-POLL] handler error:', e instanceof Error ? e.message : e));
1389
1391
  }
1390
1392
  catch (err) {
1391
1393
  console.error('📥 [ADOPT-POLL] error:', err instanceof Error ? err.message : err);
@@ -3067,7 +3069,13 @@ async function main() {
3067
3069
  console.error('❌ Reconnect attempt threw (should not happen — connectWithRetry loops):', err);
3068
3070
  });
3069
3071
  });
3070
- room.on(RoomEvent.ParticipantConnected, async (participant) => {
3072
+ // 0.9.81: handler extracted to a named reference so the adopt-poll and
3073
+ // /connect-room adopt can CALL it directly. Synthetic `room.emit(...)` does
3074
+ // NOT reach listeners on rtc-node's Room (observed 2026-07-28: ADOPT-POLL
3075
+ // re-emitted for three successive participants, zero sessions created) —
3076
+ // the event dispatch is internal, so direct invocation is the only
3077
+ // reliable path for state-driven adoption.
3078
+ participantConnectedHandler = async (participant) => {
3071
3079
  console.log(`\n👤 User joined: ${participant.identity}`);
3072
3080
  // A user is present — cancel any pending agent-side "alone" leave.
3073
3081
  if (aloneTimer) {
@@ -3929,7 +3937,8 @@ async function main() {
3929
3937
  catch (err) {
3930
3938
  console.error('❌ Failed to start session:', err);
3931
3939
  }
3932
- });
3940
+ };
3941
+ room.on(RoomEvent.ParticipantConnected, (p) => { participantConnectedHandler?.(p); });
3933
3942
  room.on(RoomEvent.ParticipantDisconnected, (participant) => {
3934
3943
  console.log(`👋 User left: ${participant.identity}`);
3935
3944
  // Full cleanup — stop all background work to avoid accumulating API usage
@@ -4549,6 +4558,33 @@ async function main() {
4549
4558
  }
4550
4559
  }
4551
4560
  }
4561
+ else if (data.type === 'skill_get') {
4562
+ const folder = (data.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
4563
+ const p = join(sessionBaseDir, '.claude', 'skills', folder, 'SKILL.md');
4564
+ if (folder && existsSync(p)) {
4565
+ await sendToFrontend({ type: 'skill_content', name: folder, content: readFileSync(p, 'utf-8') });
4566
+ }
4567
+ else {
4568
+ await sendToFrontend({ type: 'skill_content', name: folder, error: 'skill not found' });
4569
+ }
4570
+ }
4571
+ else if (data.type === 'skill_remove') {
4572
+ const folder = (data.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
4573
+ const dir = join(sessionBaseDir, '.claude', 'skills', folder);
4574
+ if (!folder || !existsSync(dir)) {
4575
+ await sendToFrontend({ type: 'skill_remove_result', success: false, error: 'skill not found' });
4576
+ }
4577
+ else {
4578
+ try {
4579
+ rmSync(dir, { recursive: true, force: true });
4580
+ console.log(`🗑️ Skill removed: ${folder}`);
4581
+ await sendToFrontend({ type: 'skill_remove_result', success: true, skills: loadSkillsList(sessionBaseDir) });
4582
+ }
4583
+ catch (err) {
4584
+ await sendToFrontend({ type: 'skill_remove_result', success: false, error: String(err) });
4585
+ }
4586
+ }
4587
+ }
4552
4588
  else if (data.type === 'join_meeting') {
4553
4589
  const meetingUrl = data.url;
4554
4590
  if (meetingUrl) {
@@ -4810,8 +4846,8 @@ async function main() {
4810
4846
  // Re-emit the event for every pre-existing participant so the session is
4811
4847
  // created exactly as if they had joined after us.
4812
4848
  for (const p of room.remoteParticipants.values()) {
4813
- console.log(`👥 Participant already in room at join: ${p.identity} — adopting (re-emitting ParticipantConnected)`);
4814
- room.emit(RoomEvent.ParticipantConnected, p);
4849
+ console.log(`👥 Participant already in room at join: ${p.identity} — adopting (invoking handler directly)`);
4850
+ participantConnectedHandler?.(p).catch((e) => console.error('👥 adopt handler error:', e instanceof Error ? e.message : e));
4815
4851
  }
4816
4852
  };
4817
4853
  // 0.9.73: explicit frontend leave should also start the idle-exit countdown —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.80",
3
+ "version": "0.9.82",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {