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.
- package/dist/claude-handler.d.ts +6 -1
- package/dist/claude-handler.js +123 -32
- package/dist/config.d.ts +33 -0
- package/dist/config.js +127 -0
- package/dist/index.js +81 -22
- package/package.json +1 -1
package/dist/claude-handler.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/claude-handler.js
CHANGED
|
@@ -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
|
-
//
|
|
9
|
-
|
|
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
|
-
|
|
152
|
+
console.log(` ๐ ${description}`);
|
|
153
|
+
this.emit('tool_use', { name: toolName, input: toolInput, description });
|
|
73
154
|
// Check if this tool needs permission
|
|
74
|
-
|
|
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,
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
174
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
52
|
+
workingDirectory: workingDir,
|
|
43
53
|
permissionMode: 'default', // Ask for permission on dangerous tools (Bash, Write, Edit)
|
|
44
|
-
|
|
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:
|
|
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
|
-
|
|
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));
|