omnikey-cli 1.0.12 → 1.0.14

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.
@@ -0,0 +1,469 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.aiClient = exports.AIClient = void 0;
7
+ exports.getDefaultModel = getDefaultModel;
8
+ const openai_1 = __importDefault(require("openai"));
9
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
10
+ const genai_1 = require("@google/genai");
11
+ const cuid_1 = __importDefault(require("cuid"));
12
+ const config_1 = require("./config");
13
+ // ---------------------------------------------------------------------------
14
+ // Default model mapping
15
+ // ---------------------------------------------------------------------------
16
+ const DEFAULT_MODELS = {
17
+ openai: { fast: 'gpt-4o-mini', smart: 'gpt-5.1' },
18
+ gemini: { fast: 'gemini-2.5-flash', smart: 'gemini-2.5-pro' },
19
+ anthropic: { fast: 'claude-haiku-4-5-20251001', smart: 'claude-sonnet-4-6' },
20
+ };
21
+ function getDefaultModel(provider, tier) {
22
+ return DEFAULT_MODELS[provider][tier];
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // OpenAI adapter
26
+ // ---------------------------------------------------------------------------
27
+ class OpenAIAdapter {
28
+ constructor(apiKey) {
29
+ this.client = new openai_1.default({ apiKey });
30
+ }
31
+ async complete(model, messages, options) {
32
+ const oaiMessages = toOpenAIMessages(messages);
33
+ const tools = options.tools?.length ? toOpenAITools(options.tools) : undefined;
34
+ const completion = await this.client.chat.completions.create({
35
+ model,
36
+ messages: oaiMessages,
37
+ tools: tools?.length ? tools : undefined,
38
+ temperature: options.temperature ?? 0.2,
39
+ max_tokens: options.maxTokens,
40
+ });
41
+ const choice = completion.choices[0];
42
+ const msg = choice.message;
43
+ const content = (msg.content ?? '').toString().trim();
44
+ const tool_calls = msg.tool_calls
45
+ ?.filter((tc) => tc.type === 'function' && 'function' in tc)
46
+ .map((tc) => ({
47
+ id: tc.id,
48
+ name: tc.function.name,
49
+ arguments: JSON.parse(tc.function.arguments || '{}'),
50
+ }));
51
+ const finishReason = choice.finish_reason === 'tool_calls'
52
+ ? 'tool_calls'
53
+ : choice.finish_reason === 'length'
54
+ ? 'length'
55
+ : 'stop';
56
+ const usage = completion.usage
57
+ ? {
58
+ prompt_tokens: completion.usage.prompt_tokens,
59
+ completion_tokens: completion.usage.completion_tokens,
60
+ total_tokens: completion.usage.total_tokens,
61
+ }
62
+ : undefined;
63
+ const assistantMessage = {
64
+ role: 'assistant',
65
+ content,
66
+ ...(tool_calls?.length ? { tool_calls } : {}),
67
+ };
68
+ return { content, finish_reason: finishReason, tool_calls, usage, model, assistantMessage };
69
+ }
70
+ async streamComplete(model, messages, options, onDelta) {
71
+ const oaiMessages = toOpenAIMessages(messages);
72
+ const stream = await this.client.chat.completions.create({
73
+ model,
74
+ messages: oaiMessages,
75
+ temperature: options.temperature ?? 0.3,
76
+ stream: true,
77
+ stream_options: { include_usage: true },
78
+ });
79
+ let usage;
80
+ for await (const part of stream) {
81
+ const delta = part.choices?.[0]?.delta?.content ?? '';
82
+ if (delta) {
83
+ onDelta(delta);
84
+ }
85
+ if (part.usage) {
86
+ usage = {
87
+ prompt_tokens: part.usage.prompt_tokens ?? 0,
88
+ completion_tokens: part.usage.completion_tokens ?? 0,
89
+ total_tokens: part.usage.total_tokens ?? 0,
90
+ };
91
+ }
92
+ }
93
+ return { usage, model };
94
+ }
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Anthropic adapter
98
+ // ---------------------------------------------------------------------------
99
+ class AnthropicAdapter {
100
+ constructor(apiKey) {
101
+ this.client = new sdk_1.default({ apiKey });
102
+ }
103
+ async complete(model, messages, options) {
104
+ const { system, messages: anthropicMessages } = toAnthropicMessages(messages);
105
+ const tools = options.tools?.length ? toAnthropicTools(options.tools) : undefined;
106
+ const response = await this.client.messages.create({
107
+ model,
108
+ max_tokens: options.maxTokens ?? 8192,
109
+ ...(system ? { system } : {}),
110
+ messages: anthropicMessages,
111
+ ...(tools?.length ? { tools } : {}),
112
+ temperature: options.temperature ?? 0.2,
113
+ });
114
+ const textContent = response.content
115
+ .filter((b) => b.type === 'text')
116
+ .map((b) => b.text)
117
+ .join('');
118
+ const tool_calls = response.content
119
+ .filter((b) => b.type === 'tool_use')
120
+ .map((b) => {
121
+ const tu = b;
122
+ return {
123
+ id: tu.id,
124
+ name: tu.name,
125
+ arguments: tu.input,
126
+ };
127
+ });
128
+ const finishReason = response.stop_reason === 'tool_use'
129
+ ? 'tool_calls'
130
+ : response.stop_reason === 'max_tokens'
131
+ ? 'length'
132
+ : 'stop';
133
+ const usage = {
134
+ prompt_tokens: response.usage.input_tokens,
135
+ completion_tokens: response.usage.output_tokens,
136
+ total_tokens: response.usage.input_tokens + response.usage.output_tokens,
137
+ };
138
+ const assistantMessage = {
139
+ role: 'assistant',
140
+ content: textContent,
141
+ ...(tool_calls?.length ? { tool_calls } : {}),
142
+ };
143
+ return {
144
+ content: textContent,
145
+ finish_reason: finishReason,
146
+ tool_calls: tool_calls?.length ? tool_calls : undefined,
147
+ usage,
148
+ model,
149
+ assistantMessage,
150
+ };
151
+ }
152
+ async streamComplete(model, messages, options, onDelta) {
153
+ const { system, messages: anthropicMessages } = toAnthropicMessages(messages);
154
+ const stream = this.client.messages.stream({
155
+ model,
156
+ max_tokens: options.maxTokens ?? 8192,
157
+ ...(system ? { system } : {}),
158
+ messages: anthropicMessages,
159
+ temperature: options.temperature ?? 0.3,
160
+ });
161
+ for await (const event of stream) {
162
+ if (event.type === 'content_block_delta' &&
163
+ event.delta.type === 'text_delta' &&
164
+ event.delta.text) {
165
+ onDelta(event.delta.text);
166
+ }
167
+ }
168
+ const finalMsg = await stream.finalMessage();
169
+ const usage = {
170
+ prompt_tokens: finalMsg.usage.input_tokens,
171
+ completion_tokens: finalMsg.usage.output_tokens,
172
+ total_tokens: finalMsg.usage.input_tokens + finalMsg.usage.output_tokens,
173
+ };
174
+ return { usage, model };
175
+ }
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Gemini adapter
179
+ // ---------------------------------------------------------------------------
180
+ class GeminiAdapter {
181
+ constructor(apiKey) {
182
+ this.client = new genai_1.GoogleGenAI({ apiKey });
183
+ }
184
+ async complete(model, messages, options) {
185
+ const { systemInstruction, contents } = toGeminiContents(messages);
186
+ const tools = options.tools?.length ? toGeminiTools(options.tools) : undefined;
187
+ const response = await this.client.models.generateContent({
188
+ model,
189
+ contents,
190
+ config: {
191
+ ...(systemInstruction ? { systemInstruction } : {}),
192
+ ...(tools?.length ? { tools } : {}),
193
+ temperature: options.temperature ?? 0.2,
194
+ },
195
+ });
196
+ const candidate = response.candidates?.[0];
197
+ const parts = candidate?.content?.parts ?? [];
198
+ const textContent = parts
199
+ .filter((p) => p.text != null)
200
+ .map((p) => p.text ?? '')
201
+ .join('');
202
+ const functionCalls = parts.filter((p) => p.functionCall != null);
203
+ const tool_calls = functionCalls.length
204
+ ? functionCalls.map((p) => ({
205
+ id: (0, cuid_1.default)(),
206
+ name: p.functionCall.name ?? '',
207
+ arguments: (p.functionCall.args ?? {}),
208
+ }))
209
+ : undefined;
210
+ const finishReason = candidate?.finishReason === 'MAX_TOKENS'
211
+ ? 'length'
212
+ : tool_calls?.length
213
+ ? 'tool_calls'
214
+ : 'stop';
215
+ const usageMeta = response.usageMetadata;
216
+ const usage = usageMeta
217
+ ? {
218
+ prompt_tokens: usageMeta.promptTokenCount ?? 0,
219
+ completion_tokens: usageMeta.candidatesTokenCount ?? 0,
220
+ total_tokens: usageMeta.totalTokenCount ?? 0,
221
+ }
222
+ : undefined;
223
+ const assistantMessage = {
224
+ role: 'assistant',
225
+ content: textContent,
226
+ ...(tool_calls?.length ? { tool_calls } : {}),
227
+ };
228
+ return {
229
+ content: textContent,
230
+ finish_reason: finishReason,
231
+ tool_calls,
232
+ usage,
233
+ model,
234
+ assistantMessage,
235
+ };
236
+ }
237
+ async streamComplete(model, messages, options, onDelta) {
238
+ const { systemInstruction, contents } = toGeminiContents(messages);
239
+ const stream = await this.client.models.generateContentStream({
240
+ model,
241
+ contents,
242
+ config: {
243
+ ...(systemInstruction ? { systemInstruction } : {}),
244
+ temperature: options.temperature ?? 0.3,
245
+ },
246
+ });
247
+ let usage;
248
+ for await (const chunk of stream) {
249
+ const text = chunk.text ?? '';
250
+ if (text) {
251
+ onDelta(text);
252
+ }
253
+ if (chunk.usageMetadata) {
254
+ usage = {
255
+ prompt_tokens: chunk.usageMetadata.promptTokenCount ?? 0,
256
+ completion_tokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
257
+ total_tokens: chunk.usageMetadata.totalTokenCount ?? 0,
258
+ };
259
+ }
260
+ }
261
+ return { usage, model };
262
+ }
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Main AIClient
266
+ // ---------------------------------------------------------------------------
267
+ class AIClient {
268
+ constructor(provider, apiKey) {
269
+ this.provider = provider;
270
+ if (provider === 'openai') {
271
+ this.openai = new OpenAIAdapter(apiKey);
272
+ }
273
+ else if (provider === 'anthropic') {
274
+ this.anthropic = new AnthropicAdapter(apiKey);
275
+ }
276
+ else if (provider === 'gemini') {
277
+ this.gemini = new GeminiAdapter(apiKey);
278
+ }
279
+ }
280
+ getProvider() {
281
+ return this.provider;
282
+ }
283
+ async complete(model, messages, options = {}) {
284
+ if (this.provider === 'openai' && this.openai) {
285
+ return this.openai.complete(model, messages, options);
286
+ }
287
+ if (this.provider === 'anthropic' && this.anthropic) {
288
+ return this.anthropic.complete(model, messages, options);
289
+ }
290
+ if (this.provider === 'gemini' && this.gemini) {
291
+ return this.gemini.complete(model, messages, options);
292
+ }
293
+ throw new Error(`AI provider "${this.provider}" is not configured.`);
294
+ }
295
+ async streamComplete(model, messages, options = {}, onDelta) {
296
+ if (this.provider === 'openai' && this.openai) {
297
+ return this.openai.streamComplete(model, messages, options, onDelta);
298
+ }
299
+ if (this.provider === 'anthropic' && this.anthropic) {
300
+ return this.anthropic.streamComplete(model, messages, options, onDelta);
301
+ }
302
+ if (this.provider === 'gemini' && this.gemini) {
303
+ return this.gemini.streamComplete(model, messages, options, onDelta);
304
+ }
305
+ throw new Error(`AI provider "${this.provider}" is not configured.`);
306
+ }
307
+ }
308
+ exports.AIClient = AIClient;
309
+ // ---------------------------------------------------------------------------
310
+ // Message format converters — OpenAI
311
+ // ---------------------------------------------------------------------------
312
+ function toOpenAIMessages(messages) {
313
+ const result = [];
314
+ for (const msg of messages) {
315
+ if (msg.role === 'system') {
316
+ result.push({ role: 'system', content: msg.content });
317
+ }
318
+ else if (msg.role === 'user') {
319
+ result.push({ role: 'user', content: msg.content });
320
+ }
321
+ else if (msg.role === 'assistant') {
322
+ if (msg.tool_calls?.length) {
323
+ result.push({
324
+ role: 'assistant',
325
+ content: msg.content || null,
326
+ tool_calls: msg.tool_calls.map((tc) => ({
327
+ id: tc.id,
328
+ type: 'function',
329
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
330
+ })),
331
+ });
332
+ }
333
+ else {
334
+ result.push({ role: 'assistant', content: msg.content });
335
+ }
336
+ }
337
+ else if (msg.role === 'tool' && msg.tool_call_id) {
338
+ result.push({
339
+ role: 'tool',
340
+ tool_call_id: msg.tool_call_id,
341
+ content: msg.content,
342
+ });
343
+ }
344
+ }
345
+ return result;
346
+ }
347
+ function toOpenAITools(tools) {
348
+ return tools.map((t) => ({
349
+ type: 'function',
350
+ function: {
351
+ name: t.name,
352
+ description: t.description,
353
+ parameters: t.parameters,
354
+ },
355
+ }));
356
+ }
357
+ function toAnthropicMessages(messages) {
358
+ let system;
359
+ const result = [];
360
+ for (const msg of messages) {
361
+ if (msg.role === 'system') {
362
+ // Anthropic takes system as a top-level param; concatenate if multiple
363
+ system = system ? `${system}\n${msg.content}` : msg.content;
364
+ continue;
365
+ }
366
+ if (msg.role === 'tool' && msg.tool_call_id) {
367
+ // Tool results must go into the user role
368
+ const prev = result[result.length - 1];
369
+ const toolResult = {
370
+ type: 'tool_result',
371
+ tool_use_id: msg.tool_call_id,
372
+ content: msg.content,
373
+ };
374
+ if (prev && prev.role === 'user' && Array.isArray(prev.content)) {
375
+ prev.content.push(toolResult);
376
+ }
377
+ else {
378
+ result.push({ role: 'user', content: [toolResult] });
379
+ }
380
+ continue;
381
+ }
382
+ if (msg.role === 'assistant' && msg.tool_calls?.length) {
383
+ const blocks = [];
384
+ if (msg.content) {
385
+ blocks.push({ type: 'text', text: msg.content });
386
+ }
387
+ for (const tc of msg.tool_calls) {
388
+ blocks.push({
389
+ type: 'tool_use',
390
+ id: tc.id,
391
+ name: tc.name,
392
+ input: tc.arguments,
393
+ });
394
+ }
395
+ result.push({ role: 'assistant', content: blocks });
396
+ continue;
397
+ }
398
+ result.push({
399
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
400
+ content: msg.content,
401
+ });
402
+ }
403
+ return { system, messages: result };
404
+ }
405
+ function toAnthropicTools(tools) {
406
+ return tools.map((t) => ({
407
+ name: t.name,
408
+ description: t.description,
409
+ input_schema: t.parameters,
410
+ }));
411
+ }
412
+ // ---------------------------------------------------------------------------
413
+ // Message format converters — Gemini
414
+ // ---------------------------------------------------------------------------
415
+ function toGeminiContents(messages) {
416
+ let systemInstruction;
417
+ const contents = [];
418
+ for (const msg of messages) {
419
+ if (msg.role === 'system') {
420
+ systemInstruction = systemInstruction ? `${systemInstruction}\n${msg.content}` : msg.content;
421
+ continue;
422
+ }
423
+ if (msg.role === 'tool' && msg.tool_call_id) {
424
+ // Tool responses go as user messages with functionResponse parts
425
+ const prev = contents[contents.length - 1];
426
+ const responsePart = {
427
+ functionResponse: {
428
+ name: msg.tool_name ?? 'tool',
429
+ response: { result: msg.content },
430
+ },
431
+ };
432
+ if (prev && prev.role === 'user') {
433
+ prev.parts = [...(prev.parts ?? []), responsePart];
434
+ }
435
+ else {
436
+ contents.push({ role: 'user', parts: [responsePart] });
437
+ }
438
+ continue;
439
+ }
440
+ if (msg.role === 'assistant' && msg.tool_calls?.length) {
441
+ const parts = msg.tool_calls.map((tc) => ({
442
+ functionCall: { name: tc.name, args: tc.arguments },
443
+ }));
444
+ if (msg.content) {
445
+ parts.unshift({ functionCall: undefined, text: msg.content });
446
+ }
447
+ contents.push({ role: 'model', parts });
448
+ continue;
449
+ }
450
+ const role = msg.role === 'assistant' ? 'model' : 'user';
451
+ contents.push({ role, parts: [{ text: msg.content }] });
452
+ }
453
+ return { systemInstruction, contents };
454
+ }
455
+ function toGeminiTools(tools) {
456
+ return [
457
+ {
458
+ functionDeclarations: tools.map((t) => ({
459
+ name: t.name,
460
+ description: t.description,
461
+ parameters: t.parameters,
462
+ })),
463
+ },
464
+ ];
465
+ }
466
+ // ---------------------------------------------------------------------------
467
+ // Shared singleton — import this instead of constructing a new AIClient
468
+ // ---------------------------------------------------------------------------
469
+ exports.aiClient = new AIClient(config_1.config.aiProvider, config_1.config.aiApiKey);
@@ -43,12 +43,36 @@ function getSqlitePath() {
43
43
  return defaultPath;
44
44
  return path_1.default.isAbsolute(envPath) ? envPath : path_1.default.join(homeDir, '.omnikey', envPath);
45
45
  }
46
+ function getAIProvider() {
47
+ const value = getEnv('AI_PROVIDER', false);
48
+ if (value === 'gemini' || value === 'anthropic' || value === 'openai')
49
+ return value;
50
+ // Auto-detect from available keys
51
+ if (getEnv('ANTHROPIC_API_KEY', false))
52
+ return 'anthropic';
53
+ if (getEnv('GEMINI_API_KEY', false))
54
+ return 'gemini';
55
+ return 'openai';
56
+ }
57
+ function getActiveApiKey(provider) {
58
+ if (provider === 'openai')
59
+ return getEnv('OPENAI_API_KEY', true);
60
+ if (provider === 'anthropic')
61
+ return getEnv('ANTHROPIC_API_KEY', true);
62
+ if (provider === 'gemini')
63
+ return getEnv('GEMINI_API_KEY', true);
64
+ throw new Error(`Unknown AI provider: ${provider}`);
65
+ }
66
+ const _provider = getAIProvider();
46
67
  exports.config = {
47
68
  // Server
48
69
  logLevel: getEnv('LOG_LEVEL', false) || 'info',
49
70
  isLocal: getBooleanEnv('LOCAL', false),
50
- // OpenAI
51
- openaiApiKey: getEnv('OPENAI_API_KEY', true),
71
+ // AI provider
72
+ aiProvider: _provider,
73
+ aiApiKey: getActiveApiKey(_provider),
74
+ // Legacy — kept for backwards compatibility; may be undefined when using another provider
75
+ openaiApiKey: getEnv('OPENAI_API_KEY', false),
52
76
  // Database
53
77
  databaseUrl: getEnv('DATABASE_URL', getBooleanEnv('IS_SELF_HOSTED', false) ? false : true),
54
78
  dbLogging: getBooleanEnv('DB_LOGGING', false),
@@ -62,4 +86,9 @@ exports.config = {
62
86
  internalApiKey: getEnv('INTERNAL_API_KEY', false),
63
87
  port: getNumberEnv('OMNIKEY_PORT', 8080),
64
88
  isSelfHosted: getBooleanEnv('IS_SELF_HOSTED', false),
89
+ // Web search providers (all optional — DuckDuckGo is used as free fallback)
90
+ serperApiKey: getEnv('SERPER_API_KEY', false),
91
+ braveSearchApiKey: getEnv('BRAVE_SEARCH_API_KEY', false),
92
+ tavilyApiKey: getEnv('TAVILY_API_KEY', false),
93
+ searxngUrl: getEnv('SEARXNG_URL', false),
65
94
  };
@@ -7,7 +7,6 @@ exports.getPromptForCommand = getPromptForCommand;
7
7
  exports.runEnhancementModel = runEnhancementModel;
8
8
  exports.createFeatureRouter = createFeatureRouter;
9
9
  const express_1 = __importDefault(require("express"));
10
- const openai_1 = __importDefault(require("openai"));
11
10
  const zod_1 = __importDefault(require("zod"));
12
11
  const types_1 = require("./types");
13
12
  const prompts_1 = require("./prompts");
@@ -17,6 +16,7 @@ const subscription_1 = require("./models/subscription");
17
16
  const subscriptionUsage_1 = require("./models/subscriptionUsage");
18
17
  const compression_1 = require("./compression");
19
18
  const subscriptionTaskTemplate_1 = require("./models/subscriptionTaskTemplate");
19
+ const ai_client_1 = require("./ai-client");
20
20
  function parseImprovedTextResponse(logger, response) {
21
21
  const match = response.match(/<improved_text>([\s\S]*?)<\/improved_text>/);
22
22
  if (match && match[1]) {
@@ -25,9 +25,6 @@ function parseImprovedTextResponse(logger, response) {
25
25
  logger.warn('LLM response did not contain expected <improved_text> tags; returning raw response.');
26
26
  return response.trim();
27
27
  }
28
- const openai = new openai_1.default({
29
- apiKey: config_1.config.openaiApiKey,
30
- });
31
28
  const enhanceRequestSchema = zod_1.default.object({
32
29
  text: zod_1.default.string(),
33
30
  });
@@ -59,7 +56,13 @@ async function getPromptForCommand(logger, cmd, subscription) {
59
56
  return '';
60
57
  }
61
58
  function getModelForCommand(cmd) {
62
- return cmd === 'task' ? 'gpt-5.1' : 'gpt-4o-mini';
59
+ const tier = cmd === 'task' ? 'smart' : 'fast';
60
+ const models = {
61
+ openai: { fast: 'gpt-4o-mini', smart: 'gpt-5.1' },
62
+ gemini: { fast: 'gemini-2.5-flash', smart: 'gemini-2.5-pro' },
63
+ anthropic: { fast: 'claude-haiku-4-5-20251001', smart: 'claude-sonnet-4-6' },
64
+ };
65
+ return models[config_1.config.aiProvider]?.[tier] ?? 'gpt-4o-mini';
63
66
  }
64
67
  function createMessagesParams(cmd, input, prompt) {
65
68
  if (cmd === 'task') {
@@ -82,49 +85,27 @@ ${input}
82
85
  ];
83
86
  }
84
87
  return [
85
- {
86
- role: 'system',
87
- content: [prompt, prompts_1.OUTPUT_FORMAT_INSTRUCTION].join('\n'),
88
- },
89
- {
90
- role: 'user',
91
- content: input,
92
- },
88
+ { role: 'system', content: [prompt, prompts_1.OUTPUT_FORMAT_INSTRUCTION].join('\n') },
89
+ { role: 'user', content: input },
93
90
  ];
94
91
  }
95
92
  async function runEnhancementModel(logger, text, cmd, subscription, onDelta) {
96
93
  const trimmed = text.trim();
97
- if (!config_1.config.openaiApiKey) {
98
- logger.warn('OPENAI_API_KEY is not set; returning null from runEnhancementModel.');
99
- return new types_1.OmniKeyError('OpenAI API key is not configured.', 500);
100
- }
101
94
  const prompt = await getPromptForCommand(logger, cmd, subscription);
102
95
  if (!prompt) {
103
96
  logger.error(`No system prompt found for command: ${cmd}`);
104
97
  return new types_1.OmniKeyError(`No system prompt found for command: ${cmd}`, 404);
105
98
  }
106
99
  const model = getModelForCommand(cmd);
107
- const stream = await openai.chat.completions.create({
108
- model,
109
- messages: createMessagesParams(cmd, trimmed, prompt),
110
- temperature: 0.3,
111
- stream: true,
112
- stream_options: { include_usage: true },
113
- });
100
+ const messages = createMessagesParams(cmd, trimmed, prompt);
114
101
  let rawResponse = '';
115
102
  let usage;
116
- for await (const part of stream) {
117
- const delta = part.choices?.[0]?.delta?.content ?? '';
118
- if (delta) {
119
- rawResponse += delta;
120
- if (onDelta) {
121
- onDelta(delta);
122
- }
123
- }
124
- if (part.usage) {
125
- usage = part.usage;
126
- }
127
- }
103
+ const result = await ai_client_1.aiClient.streamComplete(model, messages, { temperature: 0.3 }, (delta) => {
104
+ rawResponse += delta;
105
+ if (onDelta)
106
+ onDelta(delta);
107
+ });
108
+ usage = result.usage;
128
109
  return { rawResponse, usage, model };
129
110
  }
130
111
  async function enhanceText(logger, text, cmd, subscription) {
@@ -7,13 +7,14 @@ const express_1 = __importDefault(require("express"));
7
7
  const cors_1 = __importDefault(require("cors"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
+ const zlib_1 = __importDefault(require("zlib"));
10
11
  const subscriptionRoutes_1 = require("./subscriptionRoutes");
11
12
  const featureRoutes_1 = require("./featureRoutes");
12
13
  const db_1 = require("./db");
13
14
  const logger_1 = require("./logger");
14
15
  const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
15
16
  const config_1 = require("./config");
16
- const agentServer_1 = require("./agentServer");
17
+ const agentServer_1 = require("./agent/agentServer");
17
18
  const app = (0, express_1.default)();
18
19
  const PORT = Number(config_1.config.port);
19
20
  app.use((0, cors_1.default)());
@@ -80,19 +81,30 @@ app.get('/macos/appcast', (req, res) => {
80
81
  // ── Windows distribution endpoints ───────────────────────────────────────────
81
82
  // These should match the values in windows/OmniKey.Windows.csproj
82
83
  // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
83
- const WIN_VERSION = '1.0';
84
- const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-x64.zip';
84
+ const WIN_VERSION = '1.1';
85
+ const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
85
86
  const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
86
87
  // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
88
+ // Streams through gzip to reduce response size on Cloud Run.
87
89
  app.get('/windows/download', (_req, res) => {
88
- res.download(WIN_ZIP_PATH, WIN_ZIP_FILENAME, (err) => {
89
- if (err) {
90
- logger_1.logger.error('Failed to send Windows ZIP for download.', { error: err });
91
- if (!res.headersSent) {
92
- res.status(500).send('Unable to download file.');
93
- }
90
+ if (!fs_1.default.existsSync(WIN_ZIP_PATH)) {
91
+ res.status(404).send('File not found.');
92
+ return;
93
+ }
94
+ res.set({
95
+ 'Content-Type': 'application/zip',
96
+ 'Content-Disposition': `attachment; filename="${WIN_ZIP_FILENAME}"`,
97
+ 'Content-Encoding': 'gzip',
98
+ });
99
+ const fileStream = fs_1.default.createReadStream(WIN_ZIP_PATH);
100
+ const gzip = zlib_1.default.createGzip();
101
+ fileStream.on('error', (err) => {
102
+ logger_1.logger.error('Failed to send Windows ZIP for download.', { error: err });
103
+ if (!res.headersSent) {
104
+ res.status(500).send('Unable to download file.');
94
105
  }
95
106
  });
107
+ fileStream.pipe(gzip).pipe(res);
96
108
  });
97
109
  // JSON update-check endpoint consumed by UpdateChecker.cs on the Windows client.
98
110
  // Returns the latest version + download URL so the client can decide whether