@open-gitagent/voice 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shreyaskapale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @open-gitagent/voice
2
+
3
+ Voice mode + web UI for [`@open-gitagent/gitagent`](https://github.com/open-gitagent/gitagent).
4
+
5
+ This package was split out of `@open-gitagent/gitagent` in core v2.0.0 so the CLI/SDK tarball stays slim and supply-chain scanners stop blocking it. The voice runtime (OpenAI Realtime + Gemini Live adapters, the file browser, the Composio toolkit bridge, the scheduler UI, the 200 KB single-file web UI) all live here.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # Both packages — voice mode works out of the box
11
+ npm install -g @open-gitagent/gitagent @open-gitagent/voice
12
+
13
+ # Then launch with voice:
14
+ gitagent --voice -d ~/assistant
15
+ ```
16
+
17
+ The `gitagent` CLI ships in `@open-gitagent/gitagent`. `--voice` dynamically loads this package; without it installed, the CLI prints an install hint and exits.
18
+
19
+ ## What's inside
20
+
21
+ - **`startVoiceServer(opts)`** — HTTP + WebSocket server (default `:3333`) with the web UI, file viewer, settings tab, skills/integrations/scheduler tabs.
22
+ - **OpenAI Realtime adapter** — GA endpoint, GA `session.update` shape, GA event names.
23
+ - **Gemini Live adapter** — Google's Live API.
24
+ - **Composio bridge** — toolkit browser and action execution from the UI.
25
+
26
+ ## Programmatic use (SDK)
27
+
28
+ ```ts
29
+ import { startVoiceServer } from "@open-gitagent/voice";
30
+
31
+ const stop = await startVoiceServer({
32
+ adapter: "openai-realtime",
33
+ adapterConfig: { apiKey: process.env.OPENAI_API_KEY! },
34
+ agentDir: "/path/to/agent",
35
+ });
36
+
37
+ // ...later
38
+ await stop();
39
+ ```
40
+
41
+ `startVoiceServer` uses `@open-gitagent/gitagent` (peer dep) for the agent loader, SDK `query()`, the message-type protocol, and the standalone scheduler. Versions are decoupled — voice declares `^2.0.0` on core and follows core's semver.
42
+
43
+ ## Why it's split
44
+
45
+ The published `@open-gitagent/gitagent@1.5.x` tarball was being blocked with HTTP 403 by supply-chain scanners. The trigger was the bundled `dist/voice/ui.html` (~3,860 LOC of inline HTML/JS/CSS) plus the unused `baileys` dependency. Splitting voice out:
46
+
47
+ - core tarball: 179.5 kB → 85.7 kB packed (−52%), 310 → 211 deps
48
+ - voice tarball: ~150 kB packed (separate install, you opt in)
49
+
50
+ ## License
51
+
52
+ MIT.
@@ -0,0 +1,26 @@
1
+ import type { GCToolDefinition } from "@open-gitagent/gitagent";
2
+ import { type ComposioToolkit, type ComposioConnection } from "./client.js";
3
+ interface ComposioAdapterOptions {
4
+ apiKey: string;
5
+ userId?: string;
6
+ }
7
+ export declare class ComposioAdapter {
8
+ private client;
9
+ private userId;
10
+ private cachedTools;
11
+ private cacheExpiry;
12
+ private static CACHE_TTL;
13
+ constructor(opts: ComposioAdapterOptions);
14
+ getTools(): Promise<GCToolDefinition[]>;
15
+ getToolsForQuery(query: string, limit?: number): Promise<GCToolDefinition[]>;
16
+ getConnectedToolkitSlugs(): Promise<string[]>;
17
+ getToolkits(): Promise<ComposioToolkit[]>;
18
+ connect(toolkit: string, redirectUrl?: string): Promise<{
19
+ connectionId: string;
20
+ redirectUrl: string;
21
+ }>;
22
+ getConnections(): Promise<ComposioConnection[]>;
23
+ disconnect(connectionId: string): Promise<void>;
24
+ private toGCTool;
25
+ }
26
+ export {};
@@ -0,0 +1,92 @@
1
+ // Converts Composio tools into GCToolDefinition[] for injection into query()
2
+ import { ComposioClient } from "./client.js";
3
+ export class ComposioAdapter {
4
+ client;
5
+ userId;
6
+ cachedTools = null;
7
+ cacheExpiry = 0;
8
+ static CACHE_TTL = 30_000; // 30s
9
+ constructor(opts) {
10
+ this.client = new ComposioClient(opts.apiKey);
11
+ this.userId = opts.userId ?? "default";
12
+ }
13
+ // Core — returns all tools for connected toolkits (cached)
14
+ async getTools() {
15
+ const now = Date.now();
16
+ if (this.cachedTools && now < this.cacheExpiry)
17
+ return this.cachedTools;
18
+ const connections = await this.client.listConnections(this.userId);
19
+ if (connections.length === 0)
20
+ return [];
21
+ // Deduplicate toolkit slugs
22
+ const slugs = [...new Set(connections.map((c) => c.toolkitSlug))];
23
+ // Fetch tools for each connected toolkit in parallel
24
+ const toolsBySlug = await Promise.all(slugs.map((slug) => this.client.listTools(slug).catch(() => [])));
25
+ const tools = [];
26
+ for (const toolGroup of toolsBySlug) {
27
+ for (const t of toolGroup) {
28
+ tools.push(this.toGCTool(t));
29
+ }
30
+ }
31
+ this.cachedTools = tools;
32
+ this.cacheExpiry = now + ComposioAdapter.CACHE_TTL;
33
+ return tools;
34
+ }
35
+ // Dynamically fetch only the relevant tools for a user query (semantic search)
36
+ async getToolsForQuery(query, limit = 15) {
37
+ const connections = await this.client.listConnections(this.userId);
38
+ if (connections.length === 0)
39
+ return [];
40
+ const slugs = [...new Set(connections.map((c) => c.toolkitSlug))];
41
+ const tools = await this.client.searchTools(query, slugs, limit);
42
+ // Sort: direct-action tools first (SEND, CREATE, LIST), drafts last
43
+ tools.sort((a, b) => {
44
+ const aIsDraft = a.slug.includes("DRAFT");
45
+ const bIsDraft = b.slug.includes("DRAFT");
46
+ if (aIsDraft !== bIsDraft)
47
+ return aIsDraft ? 1 : -1;
48
+ return 0;
49
+ });
50
+ return tools.map((t) => this.toGCTool(t));
51
+ }
52
+ // Returns deduplicated slugs of all connected toolkits
53
+ async getConnectedToolkitSlugs() {
54
+ const connections = await this.client.listConnections(this.userId);
55
+ return [...new Set(connections.map((c) => c.toolkitSlug))];
56
+ }
57
+ // Management endpoints — proxied for server routes
58
+ async getToolkits() {
59
+ return this.client.listToolkits(this.userId);
60
+ }
61
+ async connect(toolkit, redirectUrl) {
62
+ return this.client.initiateConnection(toolkit, this.userId, redirectUrl);
63
+ }
64
+ async getConnections() {
65
+ return this.client.listConnections(this.userId);
66
+ }
67
+ async disconnect(connectionId) {
68
+ await this.client.deleteConnection(connectionId);
69
+ // Invalidate cache so tools refresh on next query
70
+ this.cachedTools = null;
71
+ }
72
+ // ── Private ────────────────────────────────────────────────────────
73
+ toGCTool(t) {
74
+ const safeName = `composio_${t.toolkitSlug}_${t.slug}`.replace(/[^a-zA-Z0-9_]/g, "_");
75
+ let description = `[Composio/${t.toolkitSlug}] ${t.description}`;
76
+ if (t.slug.includes("SEND_EMAIL")) {
77
+ description += " — USE THIS to send emails directly.";
78
+ }
79
+ else if (t.slug.includes("CREATE_EMAIL_DRAFT")) {
80
+ description += " — Only use when the user explicitly asks for a draft.";
81
+ }
82
+ return {
83
+ name: safeName,
84
+ description,
85
+ inputSchema: t.parameters,
86
+ handler: async (args) => {
87
+ const result = await this.client.executeTool(t.slug, this.userId, args);
88
+ return typeof result === "string" ? result : JSON.stringify(result);
89
+ },
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,39 @@
1
+ export interface ComposioToolkit {
2
+ slug: string;
3
+ name: string;
4
+ description: string;
5
+ logo: string;
6
+ authSchemes: string[];
7
+ noAuth: boolean;
8
+ connected: boolean;
9
+ }
10
+ export interface ComposioConnection {
11
+ id: string;
12
+ toolkitSlug: string;
13
+ status: string;
14
+ createdAt: string;
15
+ }
16
+ export interface ComposioTool {
17
+ name: string;
18
+ slug: string;
19
+ description: string;
20
+ toolkitSlug: string;
21
+ parameters: Record<string, any>;
22
+ }
23
+ export declare class ComposioClient {
24
+ private apiKey;
25
+ private authConfigCache;
26
+ constructor(apiKey: string);
27
+ listToolkits(userId?: string): Promise<ComposioToolkit[]>;
28
+ searchTools(query: string, toolkitSlugs?: string[], limit?: number): Promise<ComposioTool[]>;
29
+ listTools(toolkitSlug: string): Promise<ComposioTool[]>;
30
+ getOrCreateAuthConfig(toolkitSlug: string): Promise<string>;
31
+ initiateConnection(toolkitSlug: string, userId: string, redirectUrl?: string): Promise<{
32
+ connectionId: string;
33
+ redirectUrl: string;
34
+ }>;
35
+ listConnections(userId: string): Promise<ComposioConnection[]>;
36
+ deleteConnection(id: string): Promise<void>;
37
+ executeTool(toolSlug: string, userId: string, params: Record<string, any>, connectedAccountId?: string): Promise<any>;
38
+ private request;
39
+ }
@@ -0,0 +1,170 @@
1
+ // Composio REST API v3 client — zero dependencies, uses native fetch()
2
+ const BASE_URL = "https://backend.composio.dev/api/v3";
3
+ // ── Client ───────────────────────────────────────────────────────────
4
+ export class ComposioClient {
5
+ apiKey;
6
+ // Cache auth config IDs so we don't recreate them every connect
7
+ authConfigCache = new Map();
8
+ constructor(apiKey) {
9
+ this.apiKey = apiKey;
10
+ }
11
+ // List available toolkits, optionally merging connection status for a user
12
+ async listToolkits(userId) {
13
+ const resp = await this.request("GET", "/toolkits");
14
+ const toolkits = Array.isArray(resp) ? resp : (resp.items ?? resp.toolkits ?? []);
15
+ let connectedSlugs = new Set();
16
+ if (userId) {
17
+ try {
18
+ const conns = await this.listConnections(userId);
19
+ connectedSlugs = new Set(conns.map((c) => c.toolkitSlug));
20
+ }
21
+ catch {
22
+ // If connections fail, just show all as disconnected
23
+ }
24
+ }
25
+ return toolkits.map((tk) => ({
26
+ slug: tk.slug ?? "",
27
+ name: tk.name ?? tk.slug ?? "",
28
+ description: tk.meta?.description ?? tk.description ?? "",
29
+ logo: tk.meta?.logo ?? tk.logo ?? "",
30
+ authSchemes: tk.auth_schemes ?? [],
31
+ noAuth: tk.no_auth ?? false,
32
+ connected: connectedSlugs.has(tk.slug ?? ""),
33
+ }));
34
+ }
35
+ // Search tools across connected toolkits by natural language query
36
+ // Makes parallel per-toolkit requests since the API doesn't support comma-separated toolkit_slug with query
37
+ async searchTools(query, toolkitSlugs, limit = 10) {
38
+ const mapTool = (t) => ({
39
+ name: t.name ?? t.enum ?? "",
40
+ slug: t.slug ?? t.enum ?? t.name ?? "",
41
+ description: t.description ?? "",
42
+ toolkitSlug: t.toolkit?.slug ?? t.toolkit_slug ?? "",
43
+ parameters: t.input_parameters ?? t.parameters ?? t.inputParameters ?? {},
44
+ });
45
+ if (!toolkitSlugs?.length) {
46
+ const params = new URLSearchParams({ query, limit: String(limit) });
47
+ const resp = await this.request("GET", `/tools?${params}`);
48
+ const tools = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []);
49
+ return tools.map(mapTool);
50
+ }
51
+ // Parallel per-toolkit search
52
+ const perToolkit = await Promise.all(toolkitSlugs.map(async (slug) => {
53
+ try {
54
+ const params = new URLSearchParams({ query, toolkit_slug: slug, limit: String(limit) });
55
+ const resp = await this.request("GET", `/tools?${params}`);
56
+ const tools = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []);
57
+ return tools.map(mapTool);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }));
63
+ return perToolkit.flat().slice(0, limit);
64
+ }
65
+ // List tools for a specific toolkit
66
+ async listTools(toolkitSlug) {
67
+ const resp = await this.request("GET", `/tools?toolkit_slug=${encodeURIComponent(toolkitSlug)}`);
68
+ const tools = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []);
69
+ return tools.map((t) => ({
70
+ name: t.name ?? t.enum ?? "",
71
+ slug: t.slug ?? t.enum ?? t.name ?? "",
72
+ description: t.description ?? "",
73
+ toolkitSlug,
74
+ parameters: t.input_parameters ?? t.parameters ?? t.inputParameters ?? {},
75
+ }));
76
+ }
77
+ // Get or create an auth config for a toolkit (needed before creating a connection)
78
+ async getOrCreateAuthConfig(toolkitSlug) {
79
+ // Check cache first
80
+ const cached = this.authConfigCache.get(toolkitSlug);
81
+ if (cached)
82
+ return cached;
83
+ // Check if one already exists
84
+ const existing = await this.request("GET", `/auth_configs?toolkit_slug=${encodeURIComponent(toolkitSlug)}`);
85
+ const items = existing.items ?? [];
86
+ if (items.length > 0) {
87
+ const id = items[0].id ?? items[0].auth_config?.id;
88
+ if (id) {
89
+ this.authConfigCache.set(toolkitSlug, id);
90
+ return id;
91
+ }
92
+ }
93
+ // Create a new one with Composio-managed auth
94
+ const created = await this.request("POST", "/auth_configs", {
95
+ toolkit: { slug: toolkitSlug },
96
+ auth_scheme: "OAUTH2",
97
+ use_composio_auth: true,
98
+ });
99
+ const id = created.auth_config?.id ?? created.id ?? "";
100
+ if (id)
101
+ this.authConfigCache.set(toolkitSlug, id);
102
+ return id;
103
+ }
104
+ // Start OAuth connection flow (two-step: ensure auth config, then create connection)
105
+ async initiateConnection(toolkitSlug, userId, redirectUrl) {
106
+ const authConfigId = await this.getOrCreateAuthConfig(toolkitSlug);
107
+ if (!authConfigId) {
108
+ throw new Error(`Failed to get auth config for toolkit: ${toolkitSlug}`);
109
+ }
110
+ const body = {
111
+ auth_config: { id: authConfigId },
112
+ connection: {
113
+ user_id: userId,
114
+ ...(redirectUrl ? { callback_url: redirectUrl } : {}),
115
+ },
116
+ };
117
+ const resp = await this.request("POST", "/connected_accounts", body);
118
+ return {
119
+ connectionId: resp.id ?? "",
120
+ redirectUrl: resp.redirect_url ?? resp.redirect_uri ?? resp.redirectUrl ?? resp.redirectUri ?? "",
121
+ };
122
+ }
123
+ // List active connections for a user
124
+ async listConnections(userId) {
125
+ const resp = await this.request("GET", `/connected_accounts?user_ids=${encodeURIComponent(userId)}&statuses=ACTIVE`);
126
+ const items = Array.isArray(resp) ? resp : (resp.items ?? resp.connections ?? []);
127
+ return items.map((c) => ({
128
+ id: c.id ?? "",
129
+ toolkitSlug: c.toolkit?.slug ?? c.toolkit_slug ?? c.appUniqueId ?? c.integrationId ?? "",
130
+ status: c.status ?? "ACTIVE",
131
+ createdAt: c.createdAt ?? c.created_at ?? "",
132
+ }));
133
+ }
134
+ // Delete a connection
135
+ async deleteConnection(id) {
136
+ await this.request("DELETE", `/connected_accounts/${encodeURIComponent(id)}`);
137
+ }
138
+ // Execute a tool action
139
+ async executeTool(toolSlug, userId, params, connectedAccountId) {
140
+ const body = {
141
+ arguments: params,
142
+ user_id: userId,
143
+ };
144
+ if (connectedAccountId)
145
+ body.connected_account_id = connectedAccountId;
146
+ return this.request("POST", `/tools/execute/${encodeURIComponent(toolSlug)}`, body);
147
+ }
148
+ // ── Private ────────────────────────────────────────────────────────
149
+ async request(method, path, body) {
150
+ const url = `${BASE_URL}${path}`;
151
+ const headers = {
152
+ "x-api-key": this.apiKey,
153
+ "Accept": "application/json",
154
+ };
155
+ if (body)
156
+ headers["Content-Type"] = "application/json";
157
+ const resp = await fetch(url, {
158
+ method,
159
+ headers,
160
+ body: body ? JSON.stringify(body) : undefined,
161
+ });
162
+ if (!resp.ok) {
163
+ const text = await resp.text().catch(() => "");
164
+ throw new Error(`Composio API ${method} ${path} failed (${resp.status}): ${text}`);
165
+ }
166
+ if (resp.status === 204)
167
+ return undefined;
168
+ return resp.json();
169
+ }
170
+ }
@@ -0,0 +1,2 @@
1
+ export { ComposioClient, type ComposioToolkit, type ComposioConnection, type ComposioTool } from "./client.js";
2
+ export { ComposioAdapter } from "./adapter.js";
@@ -0,0 +1,2 @@
1
+ export { ComposioClient } from "./client.js";
2
+ export { ComposioAdapter } from "./adapter.js";
@@ -0,0 +1,20 @@
1
+ import { type MultimodalAdapter, type MultimodalAdapterConfig, type ClientMessage, type ServerMessage } from "@open-gitagent/gitagent";
2
+ export declare class GeminiLiveAdapter implements MultimodalAdapter {
3
+ private ws;
4
+ private config;
5
+ private onMessage;
6
+ private toolHandler;
7
+ private setupDone;
8
+ constructor(config: MultimodalAdapterConfig);
9
+ connect(opts: {
10
+ toolHandler: (query: string) => Promise<string>;
11
+ onMessage: (msg: ServerMessage) => void;
12
+ }): Promise<void>;
13
+ send(msg: ClientMessage): void;
14
+ disconnect(): Promise<void>;
15
+ private emit;
16
+ private sendSetup;
17
+ private handleGeminiMessage;
18
+ private handleToolCall;
19
+ private sendRaw;
20
+ }