osborn 0.1.6 → 0.5.3
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/.env.example +8 -1
- package/dist/bridge-llm.d.ts +22 -0
- package/dist/bridge-llm.js +39 -0
- package/dist/claude-handler.d.ts +6 -0
- package/dist/claude-handler.js +43 -1
- package/dist/claude-llm.d.ts +128 -0
- package/dist/claude-llm.js +623 -0
- package/dist/codex-llm.d.ts +40 -0
- package/dist/codex-llm.js +144 -0
- package/dist/config.d.ts +227 -1
- package/dist/config.js +775 -8
- package/dist/conversation-brain.d.ts +92 -0
- package/dist/conversation-brain.js +360 -0
- package/dist/fast-brain.d.ts +122 -0
- package/dist/fast-brain.js +1404 -0
- package/dist/index.js +1997 -322
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.js +610 -0
- package/dist/session-access.d.ts +399 -0
- package/dist/session-access.js +775 -0
- package/dist/smithery-proxy.d.ts +57 -0
- package/dist/smithery-proxy.js +195 -0
- package/dist/status-manager.d.ts +90 -0
- package/dist/status-manager.js +187 -0
- package/dist/voice-io.d.ts +70 -0
- package/dist/voice-io.js +152 -0
- package/package.json +17 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smithery MCP Proxy — Bridges Smithery cloud MCP servers to Claude Agent SDK.
|
|
3
|
+
*
|
|
4
|
+
* The Claude Agent SDK's `type: 'http'` transport has a known bug (#18296, #7290)
|
|
5
|
+
* where it forces OAuth discovery on all HTTP MCP servers, ignoring configured
|
|
6
|
+
* auth headers. This proxy bypasses that by:
|
|
7
|
+
*
|
|
8
|
+
* 1. Using @smithery/api/mcp createConnection() to get a working transport
|
|
9
|
+
* 2. Connecting an MCP Client to the remote server via that transport
|
|
10
|
+
* 3. Listing available tools from the remote server
|
|
11
|
+
* 4. Creating a local McpServer with proxied tool handlers
|
|
12
|
+
* 5. Returning it as { type: 'sdk', name, instance } for the Claude Agent SDK
|
|
13
|
+
*
|
|
14
|
+
* This means the Claude SDK talks to our local McpServer (in-process, no HTTP),
|
|
15
|
+
* and our McpServer forwards tool calls to Smithery via the working transport.
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
|
+
import { SmitheryAuthorizationError } from '@smithery/api/mcp';
|
|
19
|
+
export interface SmitheryProxyConfig {
|
|
20
|
+
/** Display name for the MCP server (e.g., 'youtube') */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Smithery Connect namespace (e.g., 'deer-y2fs') */
|
|
23
|
+
namespace: string;
|
|
24
|
+
/** Smithery Connect connection ID (e.g., 'youtube-mcp-sfiorini-TRmB') */
|
|
25
|
+
connectionId: string;
|
|
26
|
+
/** Upstream MCP server URL (e.g., 'https://youtube-mcp--sfiorini.run.tools') */
|
|
27
|
+
mcpUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a Smithery Connect URL into namespace and connectionId.
|
|
31
|
+
* URL format: https://api.smithery.ai/connect/{namespace}/{connectionId}/mcp
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseSmitheryUrl(url: string): {
|
|
34
|
+
namespace: string;
|
|
35
|
+
connectionId: string;
|
|
36
|
+
} | null;
|
|
37
|
+
/**
|
|
38
|
+
* Create a proxy MCP server for a Smithery-hosted server.
|
|
39
|
+
*
|
|
40
|
+
* Returns a config object compatible with Claude Agent SDK's McpSdkServerConfigWithInstance.
|
|
41
|
+
* The proxy connects to Smithery via createConnection(), discovers tools, and registers
|
|
42
|
+
* them on a local McpServer that forwards calls to the remote server.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSmitheryProxy(config: SmitheryProxyConfig): Promise<{
|
|
45
|
+
type: 'sdk';
|
|
46
|
+
name: string;
|
|
47
|
+
instance: McpServer;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Destroy an active proxy connection and clean up resources.
|
|
51
|
+
*/
|
|
52
|
+
export declare function destroySmitheryProxy(name: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Check if a URL is a Smithery Connect URL.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isSmitheryUrl(url: string): boolean;
|
|
57
|
+
export { SmitheryAuthorizationError };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smithery MCP Proxy — Bridges Smithery cloud MCP servers to Claude Agent SDK.
|
|
3
|
+
*
|
|
4
|
+
* The Claude Agent SDK's `type: 'http'` transport has a known bug (#18296, #7290)
|
|
5
|
+
* where it forces OAuth discovery on all HTTP MCP servers, ignoring configured
|
|
6
|
+
* auth headers. This proxy bypasses that by:
|
|
7
|
+
*
|
|
8
|
+
* 1. Using @smithery/api/mcp createConnection() to get a working transport
|
|
9
|
+
* 2. Connecting an MCP Client to the remote server via that transport
|
|
10
|
+
* 3. Listing available tools from the remote server
|
|
11
|
+
* 4. Creating a local McpServer with proxied tool handlers
|
|
12
|
+
* 5. Returning it as { type: 'sdk', name, instance } for the Claude Agent SDK
|
|
13
|
+
*
|
|
14
|
+
* This means the Claude SDK talks to our local McpServer (in-process, no HTTP),
|
|
15
|
+
* and our McpServer forwards tool calls to Smithery via the working transport.
|
|
16
|
+
*/
|
|
17
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
19
|
+
import { createConnection, SmitheryAuthorizationError } from '@smithery/api/mcp';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
// Track active proxy connections for cleanup
|
|
22
|
+
const activeProxies = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Parse a Smithery Connect URL into namespace and connectionId.
|
|
25
|
+
* URL format: https://api.smithery.ai/connect/{namespace}/{connectionId}/mcp
|
|
26
|
+
*/
|
|
27
|
+
export function parseSmitheryUrl(url) {
|
|
28
|
+
const match = url.match(/api\.smithery\.ai\/connect\/([^/]+)\/([^/]+)\/mcp/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return null;
|
|
31
|
+
return { namespace: match[1], connectionId: match[2] };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert a JSON Schema property definition to a Zod type.
|
|
35
|
+
* Used to register proxy tools with proper parameter schemas so Claude
|
|
36
|
+
* knows what arguments each tool accepts.
|
|
37
|
+
*/
|
|
38
|
+
function jsonSchemaPropertyToZod(prop, required) {
|
|
39
|
+
if (!prop)
|
|
40
|
+
return required ? z.any() : z.any().optional();
|
|
41
|
+
let zodType;
|
|
42
|
+
switch (prop.type) {
|
|
43
|
+
case 'string':
|
|
44
|
+
zodType = prop.enum ? z.enum(prop.enum) : z.string();
|
|
45
|
+
break;
|
|
46
|
+
case 'number':
|
|
47
|
+
case 'integer':
|
|
48
|
+
zodType = z.number();
|
|
49
|
+
break;
|
|
50
|
+
case 'boolean':
|
|
51
|
+
zodType = z.boolean();
|
|
52
|
+
break;
|
|
53
|
+
case 'array':
|
|
54
|
+
zodType = z.array(z.any());
|
|
55
|
+
break;
|
|
56
|
+
case 'object':
|
|
57
|
+
zodType = z.record(z.string(), z.any());
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
zodType = z.any();
|
|
61
|
+
}
|
|
62
|
+
if (prop.description) {
|
|
63
|
+
zodType = zodType.describe(prop.description);
|
|
64
|
+
}
|
|
65
|
+
if (!required) {
|
|
66
|
+
zodType = zodType.optional();
|
|
67
|
+
}
|
|
68
|
+
return zodType;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert a JSON Schema object to a Zod raw shape for McpServer.tool() registration.
|
|
72
|
+
*/
|
|
73
|
+
function jsonSchemaToZodShape(inputSchema) {
|
|
74
|
+
const shape = {};
|
|
75
|
+
if (!inputSchema?.properties)
|
|
76
|
+
return shape;
|
|
77
|
+
const requiredFields = inputSchema.required || [];
|
|
78
|
+
for (const [propName, propDef] of Object.entries(inputSchema.properties)) {
|
|
79
|
+
const isRequired = requiredFields.includes(propName);
|
|
80
|
+
shape[propName] = jsonSchemaPropertyToZod(propDef, isRequired);
|
|
81
|
+
}
|
|
82
|
+
return shape;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a proxy MCP server for a Smithery-hosted server.
|
|
86
|
+
*
|
|
87
|
+
* Returns a config object compatible with Claude Agent SDK's McpSdkServerConfigWithInstance.
|
|
88
|
+
* The proxy connects to Smithery via createConnection(), discovers tools, and registers
|
|
89
|
+
* them on a local McpServer that forwards calls to the remote server.
|
|
90
|
+
*/
|
|
91
|
+
export async function createSmitheryProxy(config) {
|
|
92
|
+
const { name, namespace, connectionId, mcpUrl } = config;
|
|
93
|
+
console.log(`🔌 Smithery proxy: connecting to ${name} (${namespace}/${connectionId})...`);
|
|
94
|
+
// Clean up any existing proxy for this name
|
|
95
|
+
await destroySmitheryProxy(name);
|
|
96
|
+
// 1. Get authenticated transport from Smithery SDK
|
|
97
|
+
const connection = await createConnection({
|
|
98
|
+
namespace,
|
|
99
|
+
connectionId,
|
|
100
|
+
mcpUrl,
|
|
101
|
+
});
|
|
102
|
+
console.log(`🔌 Smithery proxy: transport ready for ${name} (url: ${connection.url})`);
|
|
103
|
+
// 2. Connect MCP Client to the remote server
|
|
104
|
+
const remoteClient = new Client({ name: `${name}-proxy-client`, version: '1.0.0' }, { capabilities: {} });
|
|
105
|
+
await remoteClient.connect(connection.transport);
|
|
106
|
+
console.log(`🔌 Smithery proxy: client connected to ${name}`);
|
|
107
|
+
// 3. List tools from remote server
|
|
108
|
+
const toolsList = await remoteClient.listTools();
|
|
109
|
+
console.log(`🔌 Smithery proxy: ${name} has ${toolsList.tools.length} tools: ${toolsList.tools.map(t => t.name).join(', ')}`);
|
|
110
|
+
// 4. Create local McpServer and register proxied tools
|
|
111
|
+
const proxyServer = new McpServer({ name: `${name}-proxy`, version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
112
|
+
for (const remoteTool of toolsList.tools) {
|
|
113
|
+
const zodShape = jsonSchemaToZodShape(remoteTool.inputSchema);
|
|
114
|
+
const hasParams = Object.keys(zodShape).length > 0;
|
|
115
|
+
if (hasParams) {
|
|
116
|
+
proxyServer.tool(remoteTool.name, remoteTool.description || '', zodShape, async (args) => {
|
|
117
|
+
console.log(`🔌 Smithery proxy [${name}]: calling ${remoteTool.name}`);
|
|
118
|
+
const result = await remoteClient.callTool({
|
|
119
|
+
name: remoteTool.name,
|
|
120
|
+
arguments: args,
|
|
121
|
+
});
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
proxyServer.tool(remoteTool.name, remoteTool.description || '', async () => {
|
|
127
|
+
console.log(`🔌 Smithery proxy [${name}]: calling ${remoteTool.name}`);
|
|
128
|
+
const result = await remoteClient.callTool({
|
|
129
|
+
name: remoteTool.name,
|
|
130
|
+
arguments: {},
|
|
131
|
+
});
|
|
132
|
+
return result;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Patch McpServer and its internal Server to allow reconnection across SDK query() calls.
|
|
137
|
+
// The Claude SDK connects to the McpServer on each query() but never disconnects,
|
|
138
|
+
// so the second query throws "Already connected to a transport". This patch
|
|
139
|
+
// auto-closes the previous transport before accepting a new one.
|
|
140
|
+
// We patch at both levels because McpServer.connect may throw before reaching Server.connect.
|
|
141
|
+
const proxyServerAny = proxyServer;
|
|
142
|
+
if (proxyServerAny.connect) {
|
|
143
|
+
const originalMcpConnect = proxyServerAny.connect.bind(proxyServerAny);
|
|
144
|
+
proxyServerAny.connect = async function (transport) {
|
|
145
|
+
if (this._transport) {
|
|
146
|
+
try {
|
|
147
|
+
await this._transport.close();
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
this._transport = undefined;
|
|
151
|
+
}
|
|
152
|
+
const inner = this._server;
|
|
153
|
+
if (inner?._transport) {
|
|
154
|
+
try {
|
|
155
|
+
await inner._transport.close();
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
inner._transport = undefined;
|
|
159
|
+
}
|
|
160
|
+
return originalMcpConnect(transport);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Track for cleanup
|
|
164
|
+
activeProxies.set(name, { client: remoteClient, server: proxyServer });
|
|
165
|
+
console.log(`✅ Smithery proxy: ${name} ready with ${toolsList.tools.length} tools`);
|
|
166
|
+
return {
|
|
167
|
+
type: 'sdk',
|
|
168
|
+
name,
|
|
169
|
+
instance: proxyServer,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Destroy an active proxy connection and clean up resources.
|
|
174
|
+
*/
|
|
175
|
+
export async function destroySmitheryProxy(name) {
|
|
176
|
+
const proxy = activeProxies.get(name);
|
|
177
|
+
if (proxy) {
|
|
178
|
+
try {
|
|
179
|
+
await proxy.client.close();
|
|
180
|
+
await proxy.server.close();
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
// Ignore cleanup errors
|
|
184
|
+
}
|
|
185
|
+
activeProxies.delete(name);
|
|
186
|
+
console.log(`🔌 Smithery proxy: ${name} destroyed`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if a URL is a Smithery Connect URL.
|
|
191
|
+
*/
|
|
192
|
+
export function isSmitheryUrl(url) {
|
|
193
|
+
return url.includes('api.smithery.ai/connect/');
|
|
194
|
+
}
|
|
195
|
+
export { SmitheryAuthorizationError };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Manager - Handles background task status and conversational updates
|
|
3
|
+
*
|
|
4
|
+
* This module enables a Siri-like experience where:
|
|
5
|
+
* 1. Background research/tasks run independently
|
|
6
|
+
* 2. Status updates can be polled via a tool
|
|
7
|
+
* 3. The voice LLM can naturally ask about progress
|
|
8
|
+
* 4. Results are delivered conversationally
|
|
9
|
+
*/
|
|
10
|
+
export interface TaskStatus {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'research' | 'execute' | 'search';
|
|
13
|
+
query: string;
|
|
14
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
15
|
+
result?: string;
|
|
16
|
+
startedAt: number;
|
|
17
|
+
completedAt?: number;
|
|
18
|
+
progress?: number;
|
|
19
|
+
progressUpdates: string[];
|
|
20
|
+
lastUpdate?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface StatusUpdate {
|
|
23
|
+
hasUpdates: boolean;
|
|
24
|
+
completedTasks: TaskStatus[];
|
|
25
|
+
runningTasks: TaskStatus[];
|
|
26
|
+
pendingTasks: TaskStatus[];
|
|
27
|
+
summary: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class StatusManager {
|
|
30
|
+
private tasks;
|
|
31
|
+
private lastCheckTime;
|
|
32
|
+
private conversationContext;
|
|
33
|
+
/**
|
|
34
|
+
* Start tracking a new task (generates new ID)
|
|
35
|
+
*/
|
|
36
|
+
startTask(type: TaskStatus['type'], query: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Register a task with a specific ID (used when brain already created the task)
|
|
39
|
+
*/
|
|
40
|
+
registerTask(id: string, type: TaskStatus['type'], query: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Mark a task as running
|
|
43
|
+
*/
|
|
44
|
+
markRunning(id: string, progress?: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* Add a progress update to a running task (for interim voice updates)
|
|
47
|
+
*/
|
|
48
|
+
addProgressUpdate(id: string, update: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Get the latest unspoken progress update for any running task
|
|
51
|
+
* Returns null if no new updates available
|
|
52
|
+
*/
|
|
53
|
+
getLatestProgressUpdate(): {
|
|
54
|
+
taskId: string;
|
|
55
|
+
update: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Check if there are new progress updates available
|
|
59
|
+
*/
|
|
60
|
+
hasProgressUpdates(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Complete a task with result
|
|
63
|
+
*/
|
|
64
|
+
completeTask(id: string, result: string, success?: boolean): void;
|
|
65
|
+
/**
|
|
66
|
+
* Add context from conversation
|
|
67
|
+
*/
|
|
68
|
+
addContext(context: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Get status update - called by the check_status tool
|
|
71
|
+
*/
|
|
72
|
+
getStatusUpdate(): StatusUpdate;
|
|
73
|
+
/**
|
|
74
|
+
* Check if there are completed tasks to report
|
|
75
|
+
*/
|
|
76
|
+
hasCompletedTasks(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Get a brief status for voice announcement
|
|
79
|
+
*/
|
|
80
|
+
getBriefStatus(): string;
|
|
81
|
+
/**
|
|
82
|
+
* Clear completed tasks after they've been reported
|
|
83
|
+
*/
|
|
84
|
+
clearReportedTasks(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get context summary for the brain
|
|
87
|
+
*/
|
|
88
|
+
getContextSummary(): string;
|
|
89
|
+
}
|
|
90
|
+
export declare const statusManager: StatusManager;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Manager - Handles background task status and conversational updates
|
|
3
|
+
*
|
|
4
|
+
* This module enables a Siri-like experience where:
|
|
5
|
+
* 1. Background research/tasks run independently
|
|
6
|
+
* 2. Status updates can be polled via a tool
|
|
7
|
+
* 3. The voice LLM can naturally ask about progress
|
|
8
|
+
* 4. Results are delivered conversationally
|
|
9
|
+
*/
|
|
10
|
+
export class StatusManager {
|
|
11
|
+
tasks = new Map();
|
|
12
|
+
lastCheckTime = Date.now();
|
|
13
|
+
conversationContext = [];
|
|
14
|
+
/**
|
|
15
|
+
* Start tracking a new task (generates new ID)
|
|
16
|
+
*/
|
|
17
|
+
startTask(type, query) {
|
|
18
|
+
const id = `task-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
|
|
19
|
+
return this.registerTask(id, type, query);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register a task with a specific ID (used when brain already created the task)
|
|
23
|
+
*/
|
|
24
|
+
registerTask(id, type, query) {
|
|
25
|
+
// Skip if task already exists
|
|
26
|
+
if (this.tasks.has(id)) {
|
|
27
|
+
console.log(`📋 [Status] Task already registered: ${id}`);
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
this.tasks.set(id, {
|
|
31
|
+
id,
|
|
32
|
+
type,
|
|
33
|
+
query,
|
|
34
|
+
status: 'pending',
|
|
35
|
+
startedAt: Date.now(),
|
|
36
|
+
progressUpdates: [],
|
|
37
|
+
});
|
|
38
|
+
console.log(`📋 [Status] Task registered: ${id} - ${query.substring(0, 50)}...`);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Mark a task as running
|
|
43
|
+
*/
|
|
44
|
+
markRunning(id, progress) {
|
|
45
|
+
const task = this.tasks.get(id);
|
|
46
|
+
if (task) {
|
|
47
|
+
task.status = 'running';
|
|
48
|
+
if (progress !== undefined)
|
|
49
|
+
task.progress = progress;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Add a progress update to a running task (for interim voice updates)
|
|
54
|
+
*/
|
|
55
|
+
addProgressUpdate(id, update) {
|
|
56
|
+
const task = this.tasks.get(id);
|
|
57
|
+
if (task) {
|
|
58
|
+
task.progressUpdates.push(update);
|
|
59
|
+
task.lastUpdate = update;
|
|
60
|
+
console.log(`📊 [Status] Progress update for ${id.substring(0, 12)}: ${update.substring(0, 60)}...`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the latest unspoken progress update for any running task
|
|
65
|
+
* Returns null if no new updates available
|
|
66
|
+
*/
|
|
67
|
+
getLatestProgressUpdate() {
|
|
68
|
+
const runningTasks = Array.from(this.tasks.values()).filter(t => t.status === 'running');
|
|
69
|
+
for (const task of runningTasks) {
|
|
70
|
+
if (task.lastUpdate) {
|
|
71
|
+
const update = task.lastUpdate;
|
|
72
|
+
task.lastUpdate = undefined; // Mark as consumed
|
|
73
|
+
return { taskId: task.id, update };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if there are new progress updates available
|
|
80
|
+
*/
|
|
81
|
+
hasProgressUpdates() {
|
|
82
|
+
return Array.from(this.tasks.values()).some(t => t.status === 'running' && t.lastUpdate);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Complete a task with result
|
|
86
|
+
*/
|
|
87
|
+
completeTask(id, result, success = true) {
|
|
88
|
+
const task = this.tasks.get(id);
|
|
89
|
+
if (task) {
|
|
90
|
+
task.status = success ? 'completed' : 'failed';
|
|
91
|
+
task.result = result;
|
|
92
|
+
task.completedAt = Date.now();
|
|
93
|
+
console.log(`✅ [Status] Task ${success ? 'completed' : 'failed'}: ${id}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Add context from conversation
|
|
98
|
+
*/
|
|
99
|
+
addContext(context) {
|
|
100
|
+
this.conversationContext.push(context);
|
|
101
|
+
if (this.conversationContext.length > 10) {
|
|
102
|
+
this.conversationContext.shift();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get status update - called by the check_status tool
|
|
107
|
+
*/
|
|
108
|
+
getStatusUpdate() {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const timeSinceLastCheck = now - this.lastCheckTime;
|
|
111
|
+
this.lastCheckTime = now;
|
|
112
|
+
const allTasks = Array.from(this.tasks.values());
|
|
113
|
+
const completedTasks = allTasks.filter(t => t.status === 'completed' && t.completedAt && t.completedAt > now - 60000 // Last minute
|
|
114
|
+
);
|
|
115
|
+
const runningTasks = allTasks.filter(t => t.status === 'running');
|
|
116
|
+
const pendingTasks = allTasks.filter(t => t.status === 'pending');
|
|
117
|
+
// Generate natural summary
|
|
118
|
+
let summary = '';
|
|
119
|
+
if (completedTasks.length > 0) {
|
|
120
|
+
const results = completedTasks.map(t => {
|
|
121
|
+
const shortResult = t.result?.substring(0, 200) || 'No result';
|
|
122
|
+
return `${t.query}: ${shortResult}`;
|
|
123
|
+
}).join('\n');
|
|
124
|
+
summary = `I found some results:\n${results}`;
|
|
125
|
+
}
|
|
126
|
+
else if (runningTasks.length > 0) {
|
|
127
|
+
const tasks = runningTasks.map(t => t.query).join(', ');
|
|
128
|
+
const elapsed = Math.round((now - runningTasks[0].startedAt) / 1000);
|
|
129
|
+
summary = `Still working on: ${tasks} (${elapsed}s elapsed)`;
|
|
130
|
+
}
|
|
131
|
+
else if (pendingTasks.length > 0) {
|
|
132
|
+
summary = `${pendingTasks.length} tasks queued, starting soon...`;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
summary = "No active tasks. What would you like me to work on?";
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
hasUpdates: completedTasks.length > 0,
|
|
139
|
+
completedTasks,
|
|
140
|
+
runningTasks,
|
|
141
|
+
pendingTasks,
|
|
142
|
+
summary,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if there are completed tasks to report
|
|
147
|
+
*/
|
|
148
|
+
hasCompletedTasks() {
|
|
149
|
+
return Array.from(this.tasks.values()).some(t => t.status === 'completed' &&
|
|
150
|
+
t.completedAt &&
|
|
151
|
+
t.completedAt > this.lastCheckTime);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get a brief status for voice announcement
|
|
155
|
+
*/
|
|
156
|
+
getBriefStatus() {
|
|
157
|
+
const running = Array.from(this.tasks.values()).filter(t => t.status === 'running');
|
|
158
|
+
const completed = Array.from(this.tasks.values()).filter(t => t.status === 'completed');
|
|
159
|
+
if (running.length > 0) {
|
|
160
|
+
return `Working on ${running.length} task${running.length > 1 ? 's' : ''}...`;
|
|
161
|
+
}
|
|
162
|
+
if (completed.length > 0) {
|
|
163
|
+
return `I have ${completed.length} result${completed.length > 1 ? 's' : ''} ready.`;
|
|
164
|
+
}
|
|
165
|
+
return "Ready for your next request.";
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Clear completed tasks after they've been reported
|
|
169
|
+
*/
|
|
170
|
+
clearReportedTasks() {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
for (const [id, task] of this.tasks.entries()) {
|
|
173
|
+
// Remove tasks that completed more than 2 minutes ago
|
|
174
|
+
if (task.status === 'completed' && task.completedAt && task.completedAt < now - 120000) {
|
|
175
|
+
this.tasks.delete(id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get context summary for the brain
|
|
181
|
+
*/
|
|
182
|
+
getContextSummary() {
|
|
183
|
+
return this.conversationContext.slice(-5).join(' | ');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Singleton instance
|
|
187
|
+
export const statusManager = new StatusManager();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice I/O Module
|
|
3
|
+
* Handles STT (Speech-to-Text), TTS (Text-to-Speech), and Realtime model creation
|
|
4
|
+
*
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - Direct mode: STT (Deepgram) → Claude Agent SDK → TTS (Deepgram)
|
|
7
|
+
* - Realtime mode: OpenAI/Gemini native speech-to-speech models
|
|
8
|
+
*/
|
|
9
|
+
import * as deepgram from '@livekit/agents-plugin-deepgram';
|
|
10
|
+
import * as google from '@livekit/agents-plugin-google';
|
|
11
|
+
import * as openai from '@livekit/agents-plugin-openai';
|
|
12
|
+
import * as silero from '@livekit/agents-plugin-silero';
|
|
13
|
+
import type { RealtimeConfig } from './config.js';
|
|
14
|
+
export interface STTConfig {
|
|
15
|
+
provider: 'deepgram' | 'groq-whisper' | 'openai-whisper';
|
|
16
|
+
model?: string;
|
|
17
|
+
language?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TTSConfig {
|
|
20
|
+
provider: 'gemini' | 'openai' | 'elevenlabs' | 'deepgram';
|
|
21
|
+
voice?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface VoiceIOConfig {
|
|
25
|
+
stt: STTConfig;
|
|
26
|
+
tts: TTSConfig;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create STT (Speech-to-Text) instance based on config
|
|
30
|
+
* Note: Gemini STT is not available in Node.js, using Deepgram as default
|
|
31
|
+
*/
|
|
32
|
+
export declare function createSTT(config: STTConfig): deepgram.STT | openai.STT;
|
|
33
|
+
/**
|
|
34
|
+
* Create TTS (Text-to-Speech) instance based on config
|
|
35
|
+
* Using Gemini TTS as default (cheaper, good quality)
|
|
36
|
+
*/
|
|
37
|
+
export declare function createTTS(config: TTSConfig): any;
|
|
38
|
+
/**
|
|
39
|
+
* Create VAD (Voice Activity Detection) for turn detection
|
|
40
|
+
*
|
|
41
|
+
* Tuned to prevent:
|
|
42
|
+
* - "Audio file is too short" errors from STT (OpenAI requires >= 0.1s)
|
|
43
|
+
* - Split sentences when user pauses briefly mid-speech
|
|
44
|
+
* - False triggers from ambient noise
|
|
45
|
+
*/
|
|
46
|
+
export declare function createVAD(): Promise<silero.VAD>;
|
|
47
|
+
/**
|
|
48
|
+
* Default voice I/O configuration
|
|
49
|
+
* Uses Deepgram STT (fast, accurate) + Deepgram TTS (fast, good)
|
|
50
|
+
*/
|
|
51
|
+
export declare const DEFAULT_VOICE_IO_CONFIG: VoiceIOConfig;
|
|
52
|
+
export interface RealtimeModelConfig {
|
|
53
|
+
provider: 'openai' | 'gemini';
|
|
54
|
+
openaiVoice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
|
|
55
|
+
openaiModel?: string;
|
|
56
|
+
geminiVoice?: 'Puck' | 'Charon' | 'Kore' | 'Fenrir' | 'Aoede';
|
|
57
|
+
geminiModel?: string;
|
|
58
|
+
instructions?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create Realtime Model for native speech-to-speech
|
|
62
|
+
* Supports OpenAI Realtime API and Gemini Live API
|
|
63
|
+
*
|
|
64
|
+
* Note: Instructions are passed to voice.Agent, not to the RealtimeModel
|
|
65
|
+
*/
|
|
66
|
+
export declare function createRealtimeModel(config: RealtimeModelConfig): google.beta.realtime.RealtimeModel | openai.realtime.RealtimeModel;
|
|
67
|
+
/**
|
|
68
|
+
* Create realtime model from config
|
|
69
|
+
*/
|
|
70
|
+
export declare function createRealtimeModelFromConfig(realtimeConfig: RealtimeConfig, instructions?: string): google.beta.realtime.RealtimeModel | openai.realtime.RealtimeModel;
|