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.
- package/README.md +8 -8
- package/backend-dist/{agentPrompts.js → agent/agentPrompts.js} +18 -0
- package/backend-dist/{agentServer.js → agent/agentServer.js} +147 -54
- package/backend-dist/agent/index.js +17 -0
- package/backend-dist/agent/web-search-provider.js +135 -0
- package/backend-dist/ai-client.js +469 -0
- package/backend-dist/config.js +31 -2
- package/backend-dist/featureRoutes.js +17 -36
- package/backend-dist/index.js +21 -9
- package/dist/daemon.js +11 -3
- package/dist/index.js +7 -7
- package/dist/killDaemon.js +1 -1
- package/dist/onboard.js +97 -10
- package/dist/removeConfig.js +40 -16
- package/package.json +3 -1
- package/src/daemon.ts +19 -4
- package/src/index.ts +7 -9
- package/src/killDaemon.ts +1 -1
- package/src/onboard.ts +103 -10
- package/src/removeConfig.ts +43 -17
|
@@ -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);
|
package/backend-dist/config.js
CHANGED
|
@@ -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
|
-
//
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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) {
|
package/backend-dist/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|