osborn 0.9.96 → 0.9.98

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.
@@ -25,10 +25,11 @@ This packaged copy ships with every machine and may lag the served version.
25
25
  Check, then NOTIFY the user before updating — never silently force:
26
26
 
27
27
  ```bash
28
- SERVED_V=$(curl -sf --max-time 10 https://www.voice-native.com/api/browser-screen-recorder | grep -m1 '^Version:' | awk '{print $2}')
28
+ SERVED_V=$(curl -sf --max-time 10 https://www.voice-native.com/api/browser-screen-recorder/version | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])" 2>/dev/null)
29
29
  LOCAL_V=$(grep -m1 '^Version:' ~/.claude/skills/browser-screen-recorder/SKILL.md 2>/dev/null | awk '{print $2}')
30
30
  [ -n "$SERVED_V" ] && [ "$SERVED_V" != "$LOCAL_V" ] && echo "UPDATE AVAILABLE: browser-screen-recorder 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/browser-screen-recorder -o ~/.claude/skills/browser-screen-recorder/SKILL.md), then refresh the harness bundle per the served INSTALLATION."
31
31
  ```
32
+ (The /version route is a one-GET probe — cheap enough to run every session.)
32
33
 
33
34
  If the user approves (or has pre-approved auto-updates), update the skill
34
35
  file AND re-run the served INSTALLATION step 2 (harness bundle refresh) so
@@ -34,6 +34,12 @@ export interface ClaudeLLMOptions {
34
34
  skillNames?: string[];
35
35
  trigger?: string;
36
36
  }) => void;
37
+ agents?: Record<string, {
38
+ description: string;
39
+ prompt: string;
40
+ tools?: string[];
41
+ model?: string;
42
+ }>;
37
43
  }
38
44
  export declare const NAMED_AGENTS: {
39
45
  researcher: {
@@ -101,6 +107,14 @@ export declare class ClaudeLLM extends llm.LLM {
101
107
  label(): string;
102
108
  get model(): string;
103
109
  get sessionId(): string | null;
110
+ /**
111
+ * Set per-user named agents (DB-backed, from the frontend's set_agents).
112
+ * Synced to #opts so ClaudeLLMStream picks them up when the persistent
113
+ * query is created. SDK constraint: takes effect at the NEXT query cold
114
+ * start (session start/resume/switch) — a live subprocess keeps the agents
115
+ * it was created with.
116
+ */
117
+ setAgents(agents: ClaudeLLMOptions['agents']): void;
104
118
  /**
105
119
  * Set session ID to resume a specific conversation
106
120
  * Call this before sending the first message to resume from a previous session
@@ -459,6 +459,17 @@ export class ClaudeLLM extends llm.LLM {
459
459
  get sessionId() {
460
460
  return this.#sessionId;
461
461
  }
462
+ /**
463
+ * Set per-user named agents (DB-backed, from the frontend's set_agents).
464
+ * Synced to #opts so ClaudeLLMStream picks them up when the persistent
465
+ * query is created. SDK constraint: takes effect at the NEXT query cold
466
+ * start (session start/resume/switch) — a live subprocess keeps the agents
467
+ * it was created with.
468
+ */
469
+ setAgents(agents) {
470
+ this.#opts.agents = agents;
471
+ console.log(`🤖 Named agents ${agents ? `set (${Object.keys(agents).join(', ')})` : 'reset to built-ins'} — applies at next query cold start`);
472
+ }
462
473
  /**
463
474
  * Set session ID to resume a specific conversation
464
475
  * Call this before sending the first message to resume from a previous session
@@ -1292,9 +1303,9 @@ class ClaudeLLMStream extends llm.LLMStream {
1292
1303
  }]
1293
1304
  },
1294
1305
  // Named sub-agents — the orchestrator delegates to these specialists.
1295
- // Definitions live at module level (NAMED_AGENTS) so the HTTP API and
1296
- // frontend agents manager can list them. See top of file.
1297
- agents: NAMED_AGENTS,
1306
+ // Built-in definitions live at module level (NAMED_AGENTS); per-user
1307
+ // DB-backed definitions (opts.agents, via set_agents) take precedence.
1308
+ agents: this.#opts.agents ?? NAMED_AGENTS,
1298
1309
  };
1299
1310
  // Run Claude Agent SDK query() and stream results
1300
1311
  let hasOutput = false;
package/dist/index.js CHANGED
@@ -1602,6 +1602,16 @@ async function main() {
1602
1602
  // Empty string = anonymous / unauthenticated; uploads fall back to a
1603
1603
  // session-only path (no user prefix).
1604
1604
  let currentUserId = '';
1605
+ // Per-user named agents (DB-backed) — sent by the frontend via set_agents
1606
+ // after agent_ready. Kept here so agent-side LLM recreations (session
1607
+ // switch/resume) reuse them without waiting for a frontend resend. Merged
1608
+ // OVER the built-in NAMED_AGENTS (same-name user rows shadow built-ins).
1609
+ let userNamedAgents = null;
1610
+ // Built-ins the user has removed (tombstoned in user_agents with enabled=false).
1611
+ // A removed built-in disappears from the effective set but stays re-addable
1612
+ // from the frontend's "Available to add" list — default agents are never
1613
+ // gone forever, matching the skill-catalog pattern.
1614
+ let userRemovedAgents = [];
1605
1615
  let activeMeetingBotId = null; // Recall.ai bot ID if in a meeting
1606
1616
  let activeMeetingPoller = null; // Transcript poller bound to that bot
1607
1617
  // LIVE meeting transcript → LLM (buffered webhook finals). See recall.on('transcript').
@@ -2246,6 +2256,13 @@ async function main() {
2246
2256
  voiceMode: 'direct',
2247
2257
  skipTTSQueue: true,
2248
2258
  onCompactionEvent: buildOnCompactionEvent(),
2259
+ // Per-user named agents survive LLM recreations (session switch/resume)
2260
+ agents: (userNamedAgents || userRemovedAgents.length) ? (() => {
2261
+ const base = { ...NAMED_AGENTS };
2262
+ for (const r of userRemovedAgents)
2263
+ delete base[r];
2264
+ return { ...base, ...(userNamedAgents || {}) };
2265
+ })() : undefined,
2249
2266
  });
2250
2267
  currentLLM = directLLM;
2251
2268
  // Reset the session always-allow list for each new direct session
@@ -4834,12 +4851,61 @@ async function main() {
4834
4851
  });
4835
4852
  }
4836
4853
  else if (data.type === 'get_agents') {
4837
- // Named sub-agents for the chat agents manager — same NAMED_AGENTS
4838
- // source as GET /agents (prompts omitted).
4854
+ // Named sub-agents for the chat agents manager — built-ins (minus any
4855
+ // user-removed) merged with per-user DB-backed definitions.
4856
+ const effectiveBase = { ...NAMED_AGENTS };
4857
+ for (const r of userRemovedAgents)
4858
+ delete effectiveBase[r];
4859
+ const effective = { ...effectiveBase, ...(userNamedAgents || {}) };
4839
4860
  await sendToFrontend({
4840
4861
  type: 'agents_status',
4841
- agents: Object.entries(NAMED_AGENTS).map(([name, a]) => ({
4862
+ agents: Object.entries(effective).map(([name, a]) => ({
4842
4863
  name, description: a.description, model: a.model, tools: a.tools,
4864
+ custom: !!userNamedAgents && name in userNamedAgents,
4865
+ })),
4866
+ });
4867
+ }
4868
+ else if (data.type === 'set_agents') {
4869
+ // Per-user named agents from the DB (frontend fetches its user_agents
4870
+ // rows and sends them here after agent_ready, and again after edits).
4871
+ // Validated + merged over built-ins, applied via setAgents(). SDK
4872
+ // constraint: takes effect at the next query cold start — a live
4873
+ // subprocess keeps the agents it started with.
4874
+ const MODELS = new Set(['sonnet', 'opus', 'haiku', 'fable', 'inherit']);
4875
+ const rows = Array.isArray(data.agents) ? data.agents : [];
4876
+ const validated = {};
4877
+ for (const r of rows) {
4878
+ const name = String(r?.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
4879
+ const description = String(r?.description || '').trim();
4880
+ const prompt = String(r?.prompt || '').trim();
4881
+ if (!name || !description || !prompt)
4882
+ continue;
4883
+ if (prompt.length > 6000) {
4884
+ console.warn(`🤖 set_agents: '${name}' prompt >6KB — skipped`);
4885
+ continue;
4886
+ }
4887
+ validated[name] = {
4888
+ description,
4889
+ prompt,
4890
+ ...(Array.isArray(r.tools) && r.tools.length ? { tools: r.tools.map(String) } : {}),
4891
+ ...(r.model && (MODELS.has(r.model) || /^claude-/.test(r.model)) ? { model: String(r.model) } : {}),
4892
+ };
4893
+ }
4894
+ userNamedAgents = Object.keys(validated).length ? validated : null;
4895
+ // Removed built-ins (tombstones) — excluded from the effective set.
4896
+ userRemovedAgents = Array.isArray(data.removed) ? data.removed.map(String) : [];
4897
+ const base = { ...NAMED_AGENTS };
4898
+ for (const r of userRemovedAgents)
4899
+ delete base[r];
4900
+ const merged = { ...base, ...(userNamedAgents || {}) };
4901
+ const customized = !!userNamedAgents || userRemovedAgents.length > 0;
4902
+ currentLLM?.setAgents?.(customized ? merged : undefined);
4903
+ console.log(`🤖 set_agents: ${Object.keys(validated).length} user agent(s), ${userRemovedAgents.length} removed → effective [${Object.keys(merged).join(', ')}]`);
4904
+ await sendToFrontend({
4905
+ type: 'agents_status',
4906
+ agents: Object.entries(merged).map(([name, a]) => ({
4907
+ name, description: a.description, model: a.model, tools: a.tools,
4908
+ custom: !!userNamedAgents && name in userNamedAgents,
4843
4909
  })),
4844
4910
  });
4845
4911
  }
@@ -62,6 +62,7 @@ export declare class PipelineDirectLLM extends llm.LLM {
62
62
  };
63
63
  getMcpServers(): Record<string, import("@anthropic-ai/claude-agent-sdk").McpServerConfig>;
64
64
  setMcpServers(s: any): void;
65
+ setAgents(a: any): void;
65
66
  interruptAgent(): Promise<boolean>;
66
67
  abortAgent(): void;
67
68
  rewindAgent(checkpointId?: string): Promise<boolean>;
@@ -53,6 +53,7 @@ export class PipelineDirectLLM extends llm.LLM {
53
53
  getPendingPermission() { return this.#claudeLLM.getPendingPermission(); }
54
54
  getMcpServers() { return this.#claudeLLM.getMcpServers(); }
55
55
  setMcpServers(s) { this.#claudeLLM.setMcpServers(s); }
56
+ setAgents(a) { this.#claudeLLM.setAgents(a); }
56
57
  // Agent control — proxied to ClaudeLLM for fast brain access
57
58
  async interruptAgent() { return this.#claudeLLM.interruptQuery(); }
58
59
  abortAgent() { this.#claudeLLM.abortQuery(); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.96",
3
+ "version": "0.9.98",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {