osborn 0.9.96 → 0.9.97

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,11 @@ 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;
1605
1610
  let activeMeetingBotId = null; // Recall.ai bot ID if in a meeting
1606
1611
  let activeMeetingPoller = null; // Transcript poller bound to that bot
1607
1612
  // LIVE meeting transcript → LLM (buffered webhook finals). See recall.on('transcript').
@@ -2246,6 +2251,8 @@ async function main() {
2246
2251
  voiceMode: 'direct',
2247
2252
  skipTTSQueue: true,
2248
2253
  onCompactionEvent: buildOnCompactionEvent(),
2254
+ // Per-user named agents survive LLM recreations (session switch/resume)
2255
+ agents: userNamedAgents ? { ...NAMED_AGENTS, ...userNamedAgents } : undefined,
2249
2256
  });
2250
2257
  currentLLM = directLLM;
2251
2258
  // Reset the session always-allow list for each new direct session
@@ -4834,12 +4841,52 @@ async function main() {
4834
4841
  });
4835
4842
  }
4836
4843
  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).
4844
+ // Named sub-agents for the chat agents manager — built-ins merged with
4845
+ // any per-user DB-backed definitions (prompts omitted from the list).
4846
+ const effective = { ...NAMED_AGENTS, ...(userNamedAgents || {}) };
4839
4847
  await sendToFrontend({
4840
4848
  type: 'agents_status',
4841
- agents: Object.entries(NAMED_AGENTS).map(([name, a]) => ({
4849
+ agents: Object.entries(effective).map(([name, a]) => ({
4842
4850
  name, description: a.description, model: a.model, tools: a.tools,
4851
+ custom: !!userNamedAgents && name in userNamedAgents,
4852
+ })),
4853
+ });
4854
+ }
4855
+ else if (data.type === 'set_agents') {
4856
+ // Per-user named agents from the DB (frontend fetches its user_agents
4857
+ // rows and sends them here after agent_ready, and again after edits).
4858
+ // Validated + merged over built-ins, applied via setAgents(). SDK
4859
+ // constraint: takes effect at the next query cold start — a live
4860
+ // subprocess keeps the agents it started with.
4861
+ const MODELS = new Set(['sonnet', 'opus', 'haiku', 'fable', 'inherit']);
4862
+ const rows = Array.isArray(data.agents) ? data.agents : [];
4863
+ const validated = {};
4864
+ for (const r of rows) {
4865
+ const name = String(r?.name || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
4866
+ const description = String(r?.description || '').trim();
4867
+ const prompt = String(r?.prompt || '').trim();
4868
+ if (!name || !description || !prompt)
4869
+ continue;
4870
+ if (prompt.length > 6000) {
4871
+ console.warn(`🤖 set_agents: '${name}' prompt >6KB — skipped`);
4872
+ continue;
4873
+ }
4874
+ validated[name] = {
4875
+ description,
4876
+ prompt,
4877
+ ...(Array.isArray(r.tools) && r.tools.length ? { tools: r.tools.map(String) } : {}),
4878
+ ...(r.model && (MODELS.has(r.model) || /^claude-/.test(r.model)) ? { model: String(r.model) } : {}),
4879
+ };
4880
+ }
4881
+ userNamedAgents = Object.keys(validated).length ? validated : null;
4882
+ const merged = { ...NAMED_AGENTS, ...(userNamedAgents || {}) };
4883
+ currentLLM?.setAgents?.(userNamedAgents ? merged : undefined);
4884
+ console.log(`🤖 set_agents: ${Object.keys(validated).length} user agent(s) → effective set [${Object.keys(merged).join(', ')}]`);
4885
+ await sendToFrontend({
4886
+ type: 'agents_status',
4887
+ agents: Object.entries(merged).map(([name, a]) => ({
4888
+ name, description: a.description, model: a.model, tools: a.tools,
4889
+ custom: !!userNamedAgents && name in userNamedAgents,
4843
4890
  })),
4844
4891
  });
4845
4892
  }
@@ -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.97",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {