osborn 0.1.0 โ†’ 0.1.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.
@@ -5,6 +5,7 @@ interface ClaudeHandlerOptions {
5
5
  allowedTools?: string[];
6
6
  permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions';
7
7
  mcpServers?: Record<string, McpServerConfig>;
8
+ requireAllPermissions?: boolean;
8
9
  }
9
10
  export type { McpServerConfig };
10
11
  export interface PermissionRequestEvent {
@@ -28,10 +29,14 @@ export declare class ClaudeHandler extends EventEmitter {
28
29
  private abortController;
29
30
  private sessionId;
30
31
  private pendingPermission;
31
- private dangerousTools;
32
+ private toolStartTimes;
32
33
  private alwaysAllowedTools;
33
34
  private static readonly ALL_TOOLS;
34
35
  constructor(options?: ClaudeHandlerOptions);
36
+ /**
37
+ * Generate human-readable description for a tool call
38
+ */
39
+ private getToolDescription;
35
40
  run(prompt: string): Promise<string>;
36
41
  /**
37
42
  * Request permission from user via event emission
@@ -1,13 +1,41 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { EventEmitter } from 'events';
3
+ // Log tool calls to terminal (async, non-blocking)
4
+ function logToolCall(entry) {
5
+ // Use setImmediate to avoid blocking the main execution
6
+ setImmediate(() => {
7
+ const time = new Date().toLocaleTimeString();
8
+ const inputStr = JSON.stringify(entry.input).substring(0, 100);
9
+ switch (entry.status) {
10
+ case 'started':
11
+ console.log(`\n๐Ÿ”ง [${time}] TOOL START: ${entry.toolName}`);
12
+ console.log(` ๐Ÿ“ฅ Input: ${inputStr}${inputStr.length >= 100 ? '...' : ''}`);
13
+ break;
14
+ case 'completed':
15
+ const duration = entry.duration ? `${entry.duration}ms` : '?';
16
+ console.log(`โœ… [${time}] TOOL DONE: ${entry.toolName} (${duration})`);
17
+ if (entry.output) {
18
+ const outStr = typeof entry.output === 'string'
19
+ ? entry.output.substring(0, 150)
20
+ : JSON.stringify(entry.output).substring(0, 150);
21
+ console.log(` ๐Ÿ“ค Output: ${outStr}${outStr.length >= 150 ? '...' : ''}`);
22
+ }
23
+ break;
24
+ case 'blocked':
25
+ console.log(`โŒ [${time}] TOOL BLOCKED: ${entry.toolName}`);
26
+ console.log(` โ›” Reason: ${entry.error || 'User denied'}`);
27
+ break;
28
+ }
29
+ });
30
+ }
3
31
  export class ClaudeHandler extends EventEmitter {
4
32
  options;
5
33
  abortController = null;
6
34
  sessionId = null;
7
35
  pendingPermission = null;
8
- // Tools that require permission
9
- dangerousTools = ['Bash', 'Write', 'Edit'];
10
- // Tools the user has permanently approved
36
+ // Track tool call start times for duration logging
37
+ toolStartTimes = new Map();
38
+ // Tools the user has permanently approved (for this session)
11
39
  alwaysAllowedTools = new Set();
12
40
  // All available Claude Agent SDK tools
13
41
  static ALL_TOOLS = [
@@ -33,12 +61,52 @@ export class ClaudeHandler extends EventEmitter {
33
61
  allowedTools: options.allowedTools || ClaudeHandler.ALL_TOOLS,
34
62
  permissionMode: options.permissionMode || 'default',
35
63
  mcpServers: options.mcpServers,
64
+ // By default, require permission for ALL tools
65
+ requireAllPermissions: options.requireAllPermissions ?? true,
36
66
  };
37
67
  console.log(`๐Ÿ”ง Allowed tools: ${this.options.allowedTools?.join(', ')}`);
68
+ console.log(`๐Ÿ” Require all permissions: ${this.options.requireAllPermissions}`);
38
69
  if (this.options.mcpServers) {
39
70
  console.log(`๐Ÿ”Œ MCP servers: ${Object.keys(this.options.mcpServers).join(', ')}`);
40
71
  }
41
72
  }
73
+ /**
74
+ * Generate human-readable description for a tool call
75
+ */
76
+ getToolDescription(toolName, toolInput) {
77
+ switch (toolName) {
78
+ case 'Bash':
79
+ return `Run command: ${toolInput.command || 'unknown command'}`;
80
+ case 'Write':
81
+ return `Create file: ${toolInput.file_path || 'unknown file'}`;
82
+ case 'Edit':
83
+ return `Edit file: ${toolInput.file_path || 'unknown file'}`;
84
+ case 'MultiEdit':
85
+ return `Multi-edit: ${toolInput.edits?.length || 0} edits`;
86
+ case 'Read':
87
+ return `Read file: ${toolInput.file_path || 'unknown file'}`;
88
+ case 'Glob':
89
+ return `Find files: ${toolInput.pattern || 'unknown pattern'}`;
90
+ case 'Grep':
91
+ return `Search content: "${toolInput.pattern || 'unknown'}" in ${toolInput.path || 'cwd'}`;
92
+ case 'WebSearch':
93
+ return `๐ŸŒ Web search: "${toolInput.query || 'unknown query'}"`;
94
+ case 'WebFetch':
95
+ return `๐ŸŒ Fetch URL: ${toolInput.url || 'unknown url'}`;
96
+ case 'NotebookEdit':
97
+ return `Edit notebook: ${toolInput.notebook_path || 'unknown'}`;
98
+ case 'Task':
99
+ return `Spawn task: ${toolInput.description || 'unknown task'}`;
100
+ case 'TodoWrite':
101
+ return `Update todos: ${toolInput.todos?.length || 0} items`;
102
+ case 'LSP':
103
+ return `LSP ${toolInput.operation || 'query'}: ${toolInput.filePath || 'unknown'}`;
104
+ default:
105
+ // For MCP tools, show the tool name and first few input keys
106
+ const inputKeys = Object.keys(toolInput || {}).slice(0, 3).join(', ');
107
+ return `${toolName}: ${inputKeys || 'no params'}`;
108
+ }
109
+ }
42
110
  async run(prompt) {
43
111
  this.abortController = new AbortController();
44
112
  let fullResponse = '';
@@ -68,10 +136,26 @@ export class ClaudeHandler extends EventEmitter {
68
136
  hooks: [async (input, toolUseId) => {
69
137
  const toolName = input?.tool_name || 'unknown';
70
138
  const toolInput = input?.tool_input || {};
139
+ const id = toolUseId || `tool-${Date.now()}`;
140
+ const description = this.getToolDescription(toolName, toolInput);
141
+ // Record start time for duration tracking
142
+ this.toolStartTimes.set(id, Date.now());
143
+ // Log tool start (background, non-blocking)
144
+ logToolCall({
145
+ timestamp: new Date().toISOString(),
146
+ toolName,
147
+ toolUseId: id,
148
+ input: toolInput,
149
+ status: 'started',
150
+ });
71
151
  console.log(`๐Ÿ”ง Tool: ${toolName}`);
72
- this.emit('tool_use', { name: toolName, input: toolInput });
152
+ console.log(` ๐Ÿ“‹ ${description}`);
153
+ this.emit('tool_use', { name: toolName, input: toolInput, description });
73
154
  // Check if this tool needs permission
74
- if (this.dangerousTools.includes(toolName) && this.options.permissionMode === 'default') {
155
+ // In default mode with requireAllPermissions, ALL tools need permission
156
+ const needsPermission = this.options.permissionMode === 'default' &&
157
+ this.options.requireAllPermissions;
158
+ if (needsPermission) {
75
159
  // Skip if user has permanently approved this tool
76
160
  if (this.alwaysAllowedTools.has(toolName)) {
77
161
  console.log(`โœ… Auto-approved (always allow): ${toolName}`);
@@ -79,9 +163,18 @@ export class ClaudeHandler extends EventEmitter {
79
163
  else {
80
164
  console.log(`โš ๏ธ Permission required for: ${toolName}`);
81
165
  // Emit permission request and wait for approval
82
- const response = await this.requestPermission(toolName, toolInput, toolUseId || 'unknown');
166
+ const response = await this.requestPermission(toolName, toolInput, id, description);
83
167
  if (response === 'deny') {
84
168
  console.log(`โŒ Permission denied for: ${toolName}`);
169
+ // Log blocked tool
170
+ logToolCall({
171
+ timestamp: new Date().toISOString(),
172
+ toolName,
173
+ toolUseId: id,
174
+ input: toolInput,
175
+ status: 'blocked',
176
+ error: 'User denied permission',
177
+ });
85
178
  return {
86
179
  decision: 'block',
87
180
  reason: 'User denied permission for this operation'
@@ -101,10 +194,26 @@ export class ClaudeHandler extends EventEmitter {
101
194
  }],
102
195
  PostToolUse: [{
103
196
  matcher: '.*',
104
- hooks: [async (input) => {
197
+ hooks: [async (input, toolUseId) => {
105
198
  const toolName = input?.tool_name || 'unknown';
106
- console.log(`โœ… Completed: ${toolName}`);
107
- this.emit('tool_result', { name: toolName });
199
+ const toolOutput = input?.tool_output || input?.output;
200
+ const id = toolUseId || 'unknown';
201
+ // Calculate duration
202
+ const startTime = this.toolStartTimes.get(id);
203
+ const duration = startTime ? Date.now() - startTime : undefined;
204
+ this.toolStartTimes.delete(id);
205
+ // Log tool completion (background, non-blocking)
206
+ logToolCall({
207
+ timestamp: new Date().toISOString(),
208
+ toolName,
209
+ toolUseId: id,
210
+ input: input?.tool_input || {},
211
+ status: 'completed',
212
+ output: toolOutput ? (typeof toolOutput === 'string' ? toolOutput.substring(0, 500) : 'object') : undefined,
213
+ duration,
214
+ });
215
+ console.log(`โœ… Completed: ${toolName} (${duration ? duration + 'ms' : 'unknown duration'})`);
216
+ this.emit('tool_result', { name: toolName, output: toolOutput, duration });
108
217
  return {};
109
218
  }]
110
219
  }]
@@ -168,24 +277,15 @@ export class ClaudeHandler extends EventEmitter {
168
277
  * Request permission from user via event emission
169
278
  * Returns a promise that resolves when user responds with allow/deny/always_allow
170
279
  */
171
- requestPermission(toolName, toolInput, toolUseId) {
280
+ requestPermission(toolName, toolInput, toolUseId, description) {
172
281
  return new Promise((resolve) => {
173
- // Format the permission request message
174
- let description = '';
175
- if (toolName === 'Bash') {
176
- description = `Run command: ${toolInput.command || 'unknown command'}`;
177
- }
178
- else if (toolName === 'Write') {
179
- description = `Create file: ${toolInput.file_path || 'unknown file'}`;
180
- }
181
- else if (toolName === 'Edit') {
182
- description = `Edit file: ${toolInput.file_path || 'unknown file'}`;
183
- }
282
+ // Use provided description or generate one
283
+ const desc = description || this.getToolDescription(toolName, toolInput);
184
284
  this.pendingPermission = { toolName, toolInput, toolUseId, resolve: resolve };
185
285
  // Emit event for voice handler to pick up
186
286
  this.emit('permission_request', {
187
287
  toolName,
188
- description,
288
+ description: desc,
189
289
  toolInput,
190
290
  toolUseId,
191
291
  });
@@ -240,16 +340,7 @@ export class ClaudeHandler extends EventEmitter {
240
340
  if (!this.pendingPermission)
241
341
  return null;
242
342
  const { toolName, toolInput, toolUseId } = this.pendingPermission;
243
- let description = '';
244
- if (toolName === 'Bash') {
245
- description = `Run command: ${toolInput.command || 'unknown command'}`;
246
- }
247
- else if (toolName === 'Write') {
248
- description = `Create file: ${toolInput.file_path || 'unknown file'}`;
249
- }
250
- else if (toolName === 'Edit') {
251
- description = `Edit file: ${toolInput.file_path || 'unknown file'}`;
252
- }
343
+ const description = this.getToolDescription(toolName, toolInput);
253
344
  return { toolName, description, toolInput, toolUseId };
254
345
  }
255
346
  /**
@@ -0,0 +1,33 @@
1
+ import type { McpServerConfig } from './claude-handler.js';
2
+ export interface OsbornConfig {
3
+ workingDirectory?: string;
4
+ mcpServers?: Record<string, McpServerConfigYaml>;
5
+ defaultProvider?: 'openai' | 'gemini';
6
+ defaultCodingAgent?: 'claude' | 'codex';
7
+ }
8
+ interface McpServerConfigYaml {
9
+ enabled?: boolean;
10
+ command?: string;
11
+ args?: string[];
12
+ env?: Record<string, string>;
13
+ url?: string;
14
+ transport?: 'stdio' | 'sse' | 'http';
15
+ }
16
+ /**
17
+ * Load configuration from ~/.osborn/config.yaml
18
+ * Creates default config if it doesn't exist
19
+ */
20
+ export declare function loadConfig(): OsbornConfig;
21
+ /**
22
+ * Get enabled MCP servers in the format expected by Claude Agent SDK
23
+ */
24
+ export declare function getMcpServers(config: OsbornConfig): Record<string, McpServerConfig>;
25
+ /**
26
+ * Get list of enabled MCP server names (for display)
27
+ */
28
+ export declare function getEnabledMcpServerNames(config: OsbornConfig): string[];
29
+ /**
30
+ * Save config to file
31
+ */
32
+ export declare function saveConfig(config: OsbornConfig): void;
33
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,127 @@
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { parse, stringify } from 'yaml';
5
+ // Config file paths
6
+ const CONFIG_DIR = join(homedir(), '.osborn');
7
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
8
+ // Default config template
9
+ const DEFAULT_CONFIG = {
10
+ workingDirectory: process.cwd(),
11
+ defaultProvider: 'openai',
12
+ defaultCodingAgent: 'claude',
13
+ mcpServers: {
14
+ // Example MCP servers (disabled by default)
15
+ // github: {
16
+ // enabled: true,
17
+ // command: 'npx',
18
+ // args: ['@modelcontextprotocol/server-github'],
19
+ // env: {
20
+ // GITHUB_TOKEN: '${GITHUB_TOKEN}',
21
+ // },
22
+ // },
23
+ // filesystem: {
24
+ // enabled: true,
25
+ // command: 'npx',
26
+ // args: ['@modelcontextprotocol/server-filesystem', '/allowed/path'],
27
+ // },
28
+ },
29
+ };
30
+ /**
31
+ * Resolve environment variable references in a string
32
+ * e.g., "${GITHUB_TOKEN}" becomes the value of process.env.GITHUB_TOKEN
33
+ */
34
+ function resolveEnvVar(value) {
35
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
36
+ return process.env[envVar] || '';
37
+ });
38
+ }
39
+ /**
40
+ * Resolve environment variables in an object of strings
41
+ */
42
+ function resolveEnvVars(env) {
43
+ if (!env)
44
+ return undefined;
45
+ const resolved = {};
46
+ for (const [key, value] of Object.entries(env)) {
47
+ resolved[key] = resolveEnvVar(value);
48
+ }
49
+ return resolved;
50
+ }
51
+ /**
52
+ * Load configuration from ~/.osborn/config.yaml
53
+ * Creates default config if it doesn't exist
54
+ */
55
+ export function loadConfig() {
56
+ // Ensure config directory exists
57
+ if (!existsSync(CONFIG_DIR)) {
58
+ mkdirSync(CONFIG_DIR, { recursive: true });
59
+ console.log(`๐Ÿ“ Created config directory: ${CONFIG_DIR}`);
60
+ }
61
+ // Create default config if it doesn't exist
62
+ if (!existsSync(CONFIG_FILE)) {
63
+ const defaultYaml = stringify(DEFAULT_CONFIG);
64
+ writeFileSync(CONFIG_FILE, defaultYaml, 'utf-8');
65
+ console.log(`๐Ÿ“ Created default config: ${CONFIG_FILE}`);
66
+ return DEFAULT_CONFIG;
67
+ }
68
+ // Load and parse config
69
+ try {
70
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
71
+ const config = parse(content);
72
+ console.log(`๐Ÿ“‹ Loaded config from: ${CONFIG_FILE}`);
73
+ return config;
74
+ }
75
+ catch (err) {
76
+ console.error(`โŒ Failed to load config: ${err.message}`);
77
+ return DEFAULT_CONFIG;
78
+ }
79
+ }
80
+ /**
81
+ * Get enabled MCP servers in the format expected by Claude Agent SDK
82
+ */
83
+ export function getMcpServers(config) {
84
+ const servers = {};
85
+ if (!config.mcpServers) {
86
+ return servers;
87
+ }
88
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
89
+ // Skip disabled servers
90
+ if (serverConfig.enabled === false) {
91
+ continue;
92
+ }
93
+ // Build the McpServerConfig for Claude Agent SDK
94
+ if (serverConfig.command) {
95
+ servers[name] = {
96
+ command: serverConfig.command,
97
+ args: serverConfig.args,
98
+ env: resolveEnvVars(serverConfig.env),
99
+ };
100
+ }
101
+ // Note: SSE/HTTP transport may require different handling
102
+ }
103
+ return servers;
104
+ }
105
+ /**
106
+ * Get list of enabled MCP server names (for display)
107
+ */
108
+ export function getEnabledMcpServerNames(config) {
109
+ if (!config.mcpServers)
110
+ return [];
111
+ return Object.entries(config.mcpServers)
112
+ .filter(([_, serverConfig]) => serverConfig.enabled !== false && serverConfig.command)
113
+ .map(([name, _]) => name);
114
+ }
115
+ /**
116
+ * Save config to file
117
+ */
118
+ export function saveConfig(config) {
119
+ try {
120
+ const yaml = stringify(config);
121
+ writeFileSync(CONFIG_FILE, yaml, 'utf-8');
122
+ console.log(`๐Ÿ’พ Saved config to: ${CONFIG_FILE}`);
123
+ }
124
+ catch (err) {
125
+ console.error(`โŒ Failed to save config: ${err.message}`);
126
+ }
127
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,22 @@ import { fileURLToPath } from 'url';
6
6
  import 'dotenv/config';
7
7
  import { ClaudeHandler } from './claude-handler.js';
8
8
  import { CodexHandler } from './codex-handler.js';
9
+ import { loadConfig, getMcpServers, getEnabledMcpServerNames } from './config.js';
10
+ // Parse CLI arguments for room code
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ let roomCode;
14
+ for (let i = 0; i < args.length; i++) {
15
+ if (args[i] === '--room' && args[i + 1]) {
16
+ roomCode = args[i + 1];
17
+ }
18
+ }
19
+ return { roomCode };
20
+ }
21
+ const cliArgs = parseArgs();
22
+ if (cliArgs.roomCode) {
23
+ console.log(`๐Ÿ”— Room code provided: ${cliArgs.roomCode}`);
24
+ }
9
25
  // Global error handlers to catch silent failures
10
26
  process.on('unhandledRejection', (reason, promise) => {
11
27
  console.error('โŒ Unhandled Rejection:', reason);
@@ -21,29 +37,23 @@ if (DEBUG) {
21
37
  console.log('๐Ÿ› Debug logging enabled');
22
38
  }
23
39
  console.log(`๐Ÿค– Default LLM Provider: ${DEFAULT_PROVIDER}`);
24
- // Example MCP server configurations (uncomment to enable)
25
- const MCP_SERVERS = {
26
- // GitHub integration
27
- // 'github': {
28
- // command: 'npx',
29
- // args: ['@modelcontextprotocol/server-github'],
30
- // env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN || '' }
31
- // },
32
- // Filesystem with specific allowed paths
33
- // 'filesystem': {
34
- // command: 'npx',
35
- // args: ['@modelcontextprotocol/server-filesystem'],
36
- // env: { ALLOWED_PATHS: '/Users/newupgrade/Desktop/Developer' }
37
- // },
38
- };
40
+ // Load configuration from ~/.osborn/config.yaml
41
+ console.log('๐Ÿ“ Loading configuration...');
42
+ const config = loadConfig();
43
+ const mcpServers = getMcpServers(config);
44
+ const enabledMcpNames = getEnabledMcpServerNames(config);
45
+ if (enabledMcpNames.length > 0) {
46
+ console.log(`๐Ÿ”Œ Enabled MCP servers: ${enabledMcpNames.join(', ')}`);
47
+ }
39
48
  // Pre-initialize Claude handler at module load (before any connections)
40
49
  console.log('๐Ÿ”ฅ Pre-initializing Claude Code...');
50
+ const workingDir = config.workingDirectory || process.cwd();
41
51
  const claude = new ClaudeHandler({
42
- workingDirectory: '/Users/newupgrade/Desktop/Developer/osborn',
52
+ workingDirectory: workingDir,
43
53
  permissionMode: 'default', // Ask for permission on dangerous tools (Bash, Write, Edit)
44
- // Uncomment to enable MCP servers:
45
- // mcpServers: MCP_SERVERS,
54
+ mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
46
55
  });
56
+ console.log(`๐Ÿ“‚ Working directory: ${workingDir}`);
47
57
  // Listen for permission requests from Claude
48
58
  claude.on('permission_request', (req) => {
49
59
  console.log(`\nโš ๏ธ PERMISSION REQUIRED โš ๏ธ`);
@@ -67,6 +77,23 @@ let currentSession = null;
67
77
  // Track the current coding handler (can be Claude or Codex)
68
78
  let currentCodingAgent = 'claude';
69
79
  let codexHandler = null;
80
+ // Helper to cleanup previous session before starting new one
81
+ async function cleanupSession() {
82
+ if (currentSession) {
83
+ console.log('๐Ÿงน Cleaning up previous session...');
84
+ try {
85
+ currentSession.removeAllListeners();
86
+ // Close session gracefully if method exists
87
+ if (typeof currentSession.close === 'function') {
88
+ await currentSession.close();
89
+ }
90
+ }
91
+ catch (err) {
92
+ console.log('โš ๏ธ Session cleanup error (non-fatal):', err.message);
93
+ }
94
+ currentSession = null;
95
+ }
96
+ }
70
97
  // Helper to send data to frontend
71
98
  async function sendToFrontend(data) {
72
99
  if (!jobContext)
@@ -190,11 +217,13 @@ function createModel(provider) {
190
217
  // From official docs: https://docs.livekit.io/agents/models/realtime/plugins/gemini/
191
218
  // Package v1.0.31 uses google.beta.realtime (not google.realtime yet)
192
219
  const model = new google.beta.realtime.RealtimeModel({
193
- model: 'gemini-2.5-flash-native-audio-preview-12-2025', // From official docs
220
+ // model: 'gemini-2.5-flash-native-audio-preview-12-2025', // From official docs
221
+ model: 'gemini-3.5-flash-latest', // From official docs
194
222
  voice: 'Puck',
195
223
  instructions: OSBORN_INSTRUCTIONS,
196
224
  });
197
- console.log('โœ… Gemini model created with gemini-2.5-flash-native-audio-preview-12-2025');
225
+ // console.log('โœ… Gemini model created with gemini-2.5-flash-native-audio-preview-12-2025')
226
+ console.log('โœ… Gemini model created with gemini-3.5-flash-latest');
198
227
  return model;
199
228
  }
200
229
  else {
@@ -234,6 +263,15 @@ function getCodingAgentFromParticipant(metadata) {
234
263
  export default defineAgent({
235
264
  entry: async (ctx) => {
236
265
  console.log('๐Ÿš€ Agent starting for room:', ctx.room.name);
266
+ // If room code was provided via CLI, validate room name
267
+ if (cliArgs.roomCode) {
268
+ const expectedRoom = `osborn-${cliArgs.roomCode}`;
269
+ if (ctx.room.name !== expectedRoom) {
270
+ console.log(`โญ๏ธ Skipping room ${ctx.room.name} (waiting for ${expectedRoom})`);
271
+ return; // Don't handle this room
272
+ }
273
+ console.log(`โœ… Room matches expected: ${expectedRoom}`);
274
+ }
237
275
  jobContext = ctx;
238
276
  // Claude verbose logging
239
277
  claude.on('tool_use', (tool) => {
@@ -272,12 +310,14 @@ export default defineAgent({
272
310
  if (codingAgent === 'codex') {
273
311
  console.log('๐Ÿ”ง Initializing Codex handler...');
274
312
  codexHandler = new CodexHandler({
275
- workingDirectory: '/Users/newupgrade/Desktop/Developer/osborn',
313
+ workingDirectory: workingDir,
276
314
  });
277
315
  console.log('โœ… Codex handler ready');
278
316
  }
279
317
  // Create model based on user's choice
280
318
  const model = createModel(provider);
319
+ // Clean up any previous session before creating new one
320
+ await cleanupSession();
281
321
  const session = new voice.AgentSession({
282
322
  llm: model,
283
323
  });
@@ -302,6 +342,11 @@ export default defineAgent({
302
342
  ctx.room.on('trackSubscribed', (track, publication, p) => {
303
343
  console.log(`๐Ÿ“ฅ Track subscribed: ${track.kind} from ${p.identity}`);
304
344
  });
345
+ ctx.room.on('participantDisconnected', async (p) => {
346
+ console.log(`๐Ÿ‘‹ Participant disconnected: ${p.identity}`);
347
+ // Clean up session when user disconnects to prepare for next connection
348
+ await cleanupSession();
349
+ });
305
350
  // Listen for data channel messages from frontend
306
351
  ctx.room.on('dataReceived', async (payload, participant, kind, topic) => {
307
352
  if (topic === 'user-input') {
@@ -353,4 +398,18 @@ export default defineAgent({
353
398
  console.log('๐ŸŽค Ready for voice input! Speak to start.');
354
399
  },
355
400
  });
356
- cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) }));
401
+ // Configure server options
402
+ const serverOptions = {
403
+ agent: fileURLToPath(import.meta.url),
404
+ };
405
+ // If room code is provided, filter to only handle that room
406
+ if (cliArgs.roomCode) {
407
+ const targetRoom = `osborn-${cliArgs.roomCode}`;
408
+ console.log(`๐ŸŽฏ Filtering for room: ${targetRoom}`);
409
+ // The agent will be dispatched to rooms matching this pattern
410
+ serverOptions.workerOptions = {
411
+ // Note: Room filtering is handled by LiveKit dispatch
412
+ // For local development, we validate the room in the entry function
413
+ };
414
+ }
415
+ cli.runApp(new ServerOptions(serverOptions));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {