omnikey-cli 1.0.22 → 1.0.24
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/backend-dist/agent/agentAuth.js +134 -0
- package/backend-dist/agent/agentServer.js +20 -142
- package/backend-dist/agent/types.js +2 -0
- package/backend-dist/agent/utils.js +104 -0
- package/backend-dist/ai-client.js +40 -0
- package/backend-dist/config.js +1 -1
- package/backend-dist/index.js +28 -5
- package/backend-dist/models/appDownload.js +34 -0
- package/backend-dist/web-search-provider.js +8 -2
- package/dist/daemon.js +22 -7
- package/dist/removeConfig.js +12 -4
- package/package.json +1 -1
- package/src/daemon.ts +29 -8
- package/src/removeConfig.ts +18 -4
|
@@ -0,0 +1,134 @@
|
|
|
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.authenticateFromAuthHeader = authenticateFromAuthHeader;
|
|
7
|
+
exports.createLazyAuthContext = createLazyAuthContext;
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const subscription_1 = require("../models/subscription");
|
|
11
|
+
const authMiddleware_1 = require("../authMiddleware");
|
|
12
|
+
/**
|
|
13
|
+
* Authenticates a WebSocket connection from a Bearer token in the Authorization header.
|
|
14
|
+
*
|
|
15
|
+
* In self-hosted mode, skips JWT verification and returns the self-hosted subscription directly.
|
|
16
|
+
* Otherwise, verifies the JWT, looks up the subscription by ID, and checks that it is not expired.
|
|
17
|
+
* Marks the subscription as expired and persists the change if the license key has passed its expiry date.
|
|
18
|
+
*
|
|
19
|
+
* @param authHeader - The raw `Authorization` header value (e.g. `"Bearer <token>"`).
|
|
20
|
+
* @param log - Logger instance scoped to the current connection.
|
|
21
|
+
* @returns The authenticated `Subscription`, or `null` if authentication fails for any reason.
|
|
22
|
+
*/
|
|
23
|
+
async function authenticateFromAuthHeader(authHeader, log) {
|
|
24
|
+
if (config_1.config.isSelfHosted) {
|
|
25
|
+
log.info('Self-hosted mode: skipping JWT authentication for agent WebSocket connection.');
|
|
26
|
+
try {
|
|
27
|
+
const subscription = await (0, authMiddleware_1.selfHostedSubscription)();
|
|
28
|
+
log.info('Retrieved self-hosted subscription for agent WebSocket connection', {
|
|
29
|
+
subscriptionId: subscription.id,
|
|
30
|
+
});
|
|
31
|
+
return subscription;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
log.error('Failed to retrieve self-hosted subscription for agent WebSocket connection', {
|
|
35
|
+
error: err,
|
|
36
|
+
});
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!config_1.config.jwtSecret) {
|
|
41
|
+
log.error('JWT secret is not configured. Cannot authenticate subscription from auth header.');
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!authHeader) {
|
|
45
|
+
log.warn('Agent WebSocket connection missing authorization header');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const [scheme, token] = authHeader.split(' ');
|
|
49
|
+
if (scheme !== 'Bearer' || !token) {
|
|
50
|
+
log.warn('Agent WebSocket connection has malformed authorization header');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
|
|
55
|
+
const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
|
|
56
|
+
if (!subscription) {
|
|
57
|
+
log.warn('Agent WebSocket auth failed: subscription not found', {
|
|
58
|
+
sid: decoded.sid,
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (subscription.subscriptionStatus === 'expired') {
|
|
63
|
+
log.warn('Agent WebSocket auth failed: subscription expired', {
|
|
64
|
+
sid: decoded.sid,
|
|
65
|
+
});
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const now = new Date();
|
|
69
|
+
if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
|
|
70
|
+
subscription.subscriptionStatus = 'expired';
|
|
71
|
+
await subscription.save();
|
|
72
|
+
log.info('Agent WebSocket auth: subscription key expired during connection', {
|
|
73
|
+
subscriptionId: subscription.id,
|
|
74
|
+
});
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
log.debug('Agent WebSocket auth succeeded', {
|
|
78
|
+
subscriptionId: subscription.id,
|
|
79
|
+
status: subscription.subscriptionStatus,
|
|
80
|
+
});
|
|
81
|
+
return subscription;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
log.warn('Agent WebSocket auth failed: invalid or expired JWT', { error: err });
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Creates a lazy authentication context for a WebSocket connection.
|
|
90
|
+
*
|
|
91
|
+
* Authentication is deferred until the first call to `ensureAuthenticated`, and the result
|
|
92
|
+
* is cached so subsequent calls resolve immediately without re-verifying the token.
|
|
93
|
+
* Concurrent calls during the first authentication are coalesced into a single in-flight promise.
|
|
94
|
+
*
|
|
95
|
+
* @param authHeader - The raw `Authorization` header value forwarded from the upgrade request.
|
|
96
|
+
* @param log - Logger instance scoped to the current connection.
|
|
97
|
+
* @returns An `AuthContext` with `ensureAuthenticated` and `getSubscription` accessors.
|
|
98
|
+
*/
|
|
99
|
+
function createLazyAuthContext(authHeader, log) {
|
|
100
|
+
let authenticatedSubscription = null;
|
|
101
|
+
let authFailed = false;
|
|
102
|
+
let authPromise = null;
|
|
103
|
+
const ensureAuthenticated = async () => {
|
|
104
|
+
if (authenticatedSubscription) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (authFailed) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!authPromise) {
|
|
111
|
+
authPromise = (async () => {
|
|
112
|
+
try {
|
|
113
|
+
const sub = await authenticateFromAuthHeader(authHeader, log);
|
|
114
|
+
if (!sub) {
|
|
115
|
+
authFailed = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
authenticatedSubscription = sub;
|
|
119
|
+
log.info('Agent WebSocket authenticated', {
|
|
120
|
+
subscriptionId: authenticatedSubscription.id,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
authFailed = true;
|
|
125
|
+
log.error('Unexpected error during agent WebSocket auth', { error: err });
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
}
|
|
129
|
+
await authPromise;
|
|
130
|
+
return Boolean(authenticatedSubscription);
|
|
131
|
+
};
|
|
132
|
+
const getSubscription = () => authenticatedSubscription;
|
|
133
|
+
return { ensureAuthenticated, getSubscription };
|
|
134
|
+
}
|
|
@@ -38,7 +38,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.attachAgentWebSocketServer = attachAgentWebSocketServer;
|
|
40
40
|
const ws_1 = __importStar(require("ws"));
|
|
41
|
-
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
42
41
|
const cuid_1 = __importDefault(require("cuid"));
|
|
43
42
|
const config_1 = require("../config");
|
|
44
43
|
const logger_1 = require("../logger");
|
|
@@ -46,8 +45,9 @@ const subscription_1 = require("../models/subscription");
|
|
|
46
45
|
const subscriptionUsage_1 = require("../models/subscriptionUsage");
|
|
47
46
|
const agentPrompts_1 = require("./agentPrompts");
|
|
48
47
|
const featureRoutes_1 = require("../featureRoutes");
|
|
49
|
-
const authMiddleware_1 = require("../authMiddleware");
|
|
50
48
|
const web_search_provider_1 = require("../web-search-provider");
|
|
49
|
+
const agentAuth_1 = require("./agentAuth");
|
|
50
|
+
const utils_1 = require("./utils");
|
|
51
51
|
const ai_client_1 = require("../ai-client");
|
|
52
52
|
async function runToolLoop(initialResult, session, sessionId, send, log, tools, onUsage) {
|
|
53
53
|
const MAX_TOOL_ITERATIONS = 10;
|
|
@@ -61,7 +61,7 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
|
|
|
61
61
|
// would leave the history ending with an assistant turn, causing a 400.
|
|
62
62
|
if (!toolCalls.length)
|
|
63
63
|
break;
|
|
64
|
-
|
|
64
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, result.assistantMessage);
|
|
65
65
|
log.info('Agent executing tool calls', {
|
|
66
66
|
sessionId,
|
|
67
67
|
turn: session.turns,
|
|
@@ -91,7 +91,7 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
|
|
|
91
91
|
return { id: tc.id, name: tc.name, result: toolResult };
|
|
92
92
|
}));
|
|
93
93
|
for (const { id, name, result: toolResult } of toolResults) {
|
|
94
|
-
|
|
94
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
95
95
|
role: 'tool',
|
|
96
96
|
tool_call_id: id,
|
|
97
97
|
tool_name: name,
|
|
@@ -109,19 +109,19 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
|
|
|
109
109
|
// force a final text response by calling again without tools.
|
|
110
110
|
if (result.finish_reason === 'tool_calls') {
|
|
111
111
|
log.warn('Tool loop hit MAX_TOOL_ITERATIONS; forcing final conclusion', { sessionId });
|
|
112
|
-
|
|
112
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, result.assistantMessage);
|
|
113
113
|
// The API requires a tool_result for every tool_use in the preceding
|
|
114
114
|
// assistant message. Add synthetic results for any unexecuted calls so
|
|
115
115
|
// the history remains valid before we send the follow-up user message.
|
|
116
116
|
for (const tc of result.tool_calls ?? []) {
|
|
117
|
-
|
|
117
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
118
118
|
role: 'tool',
|
|
119
119
|
tool_call_id: tc.id,
|
|
120
120
|
tool_name: tc.name,
|
|
121
121
|
content: 'Tool call limit reached. Result unavailable.',
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
125
125
|
role: 'user',
|
|
126
126
|
content: 'You have reached the maximum number of tool calls. Do NOT make any further tool calls or web searches. You MUST now provide a final answer directly. If you still need to gather information from the system, generate a `<shell_scripts>` block instead of making tool calls.',
|
|
127
127
|
});
|
|
@@ -136,10 +136,6 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
|
|
|
136
136
|
});
|
|
137
137
|
return result;
|
|
138
138
|
}
|
|
139
|
-
function buildAvailableTools() {
|
|
140
|
-
// web_search is always available — DuckDuckGo is used as free fallback
|
|
141
|
-
return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
|
|
142
|
-
}
|
|
143
139
|
const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
|
|
144
140
|
const sessionMessages = new Map();
|
|
145
141
|
const MAX_TURNS = 10;
|
|
@@ -199,79 +195,6 @@ ${prompt}
|
|
|
199
195
|
hasStoredPrompt: !!prompt,
|
|
200
196
|
};
|
|
201
197
|
}
|
|
202
|
-
async function authenticateFromAuthHeader(authHeader, log) {
|
|
203
|
-
if (config_1.config.isSelfHosted) {
|
|
204
|
-
log.info('Self-hosted mode: skipping JWT authentication for agent WebSocket connection.');
|
|
205
|
-
try {
|
|
206
|
-
const subscription = await (0, authMiddleware_1.selfHostedSubscription)();
|
|
207
|
-
log.info('Retrieved self-hosted subscription for agent WebSocket connection', {
|
|
208
|
-
subscriptionId: subscription.id,
|
|
209
|
-
});
|
|
210
|
-
return subscription;
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
log.error('Failed to retrieve self-hosted subscription for agent WebSocket connection', {
|
|
214
|
-
error: err,
|
|
215
|
-
});
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (!config_1.config.jwtSecret) {
|
|
220
|
-
log.error('JWT secret is not configured. Cannot authenticate subscription from auth header.');
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
if (!authHeader) {
|
|
224
|
-
log.warn('Agent WebSocket connection missing authorization header');
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const [scheme, token] = authHeader.split(' ');
|
|
228
|
-
if (scheme !== 'Bearer' || !token) {
|
|
229
|
-
log.warn('Agent WebSocket connection has malformed authorization header');
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
|
|
234
|
-
const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
|
|
235
|
-
if (!subscription) {
|
|
236
|
-
log.warn('Agent WebSocket auth failed: subscription not found', {
|
|
237
|
-
sid: decoded.sid,
|
|
238
|
-
});
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
if (subscription.subscriptionStatus === 'expired') {
|
|
242
|
-
log.warn('Agent WebSocket auth failed: subscription expired', {
|
|
243
|
-
sid: decoded.sid,
|
|
244
|
-
});
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
const now = new Date();
|
|
248
|
-
if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
|
|
249
|
-
subscription.subscriptionStatus = 'expired';
|
|
250
|
-
await subscription.save();
|
|
251
|
-
log.info('Agent WebSocket auth: subscription key expired during connection', {
|
|
252
|
-
subscriptionId: subscription.id,
|
|
253
|
-
});
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
log.debug('Agent WebSocket auth succeeded', {
|
|
257
|
-
subscriptionId: subscription.id,
|
|
258
|
-
status: subscription.subscriptionStatus,
|
|
259
|
-
});
|
|
260
|
-
return subscription;
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
log.warn('Agent WebSocket auth failed: invalid or expired JWT', { error: err });
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function createUserContent(content, hasStoredPrompt) {
|
|
268
|
-
return hasStoredPrompt
|
|
269
|
-
? content
|
|
270
|
-
.toLowerCase()
|
|
271
|
-
.replace(/@omniagent/g, '')
|
|
272
|
-
.trim()
|
|
273
|
-
: content;
|
|
274
|
-
}
|
|
275
198
|
async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
276
199
|
const { sessionState: session, hasStoredPrompt } = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
|
|
277
200
|
// Count this call as one agent iteration.
|
|
@@ -284,7 +207,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
284
207
|
// On the MAX_TURNS iteration, instruct the LLM to provide a final,
|
|
285
208
|
// consolidated answer based on the full conversation context.
|
|
286
209
|
if (session.turns === MAX_TURNS) {
|
|
287
|
-
|
|
210
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
288
211
|
role: 'system',
|
|
289
212
|
content: 'Provide a single, final, concise answer based on the entire conversation so far. Wrap the answer in a <final_answer>...</final_answer> block and do not ask for further input or mention additional shell scripts to run. Do not include any <shell_script> block in this response.',
|
|
290
213
|
});
|
|
@@ -314,17 +237,17 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
314
237
|
// represent environment feedback that the agent must reason about next.
|
|
315
238
|
// Pushing them as 'assistant' would create two consecutive assistant turns
|
|
316
239
|
// which breaks most LLM APIs and prevents the model from processing the output.
|
|
317
|
-
|
|
240
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
318
241
|
role: 'user',
|
|
319
242
|
content: isAssistance
|
|
320
243
|
? userContent
|
|
321
|
-
: `<user_input>${createUserContent(userContent, hasStoredPrompt)}</user_input>`,
|
|
244
|
+
: `<user_input>${(0, utils_1.createUserContent)(userContent, hasStoredPrompt)}</user_input>`,
|
|
322
245
|
});
|
|
323
246
|
}
|
|
324
247
|
// On the final turn we omit tools so the model is forced to emit a
|
|
325
248
|
// plain text <final_answer> rather than issuing another tool call.
|
|
326
249
|
const isFinalTurn = session.turns >= MAX_TURNS;
|
|
327
|
-
const tools = isFinalTurn ? undefined : buildAvailableTools();
|
|
250
|
+
const tools = isFinalTurn ? undefined : (0, utils_1.buildAvailableTools)();
|
|
328
251
|
const recordUsage = async (result) => {
|
|
329
252
|
const usage = result.usage;
|
|
330
253
|
if (!usage || !subscription.id || config_1.config.isSelfHosted)
|
|
@@ -366,7 +289,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
366
289
|
if (!content && result.finish_reason !== 'tool_calls') {
|
|
367
290
|
log.warn('Agent LLM returned empty content; sending generic error to client.');
|
|
368
291
|
const errorMessage = 'The agent returned an empty response. Please try again.';
|
|
369
|
-
sendFinalAnswer(send, sessionId, errorMessage, true);
|
|
292
|
+
(0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
|
|
370
293
|
// Clear any cached session state so a subsequent attempt can
|
|
371
294
|
// start fresh without a polluted history.
|
|
372
295
|
sessionMessages.delete(sessionId);
|
|
@@ -380,7 +303,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
380
303
|
subscriptionId: subscription.id,
|
|
381
304
|
turn: session.turns,
|
|
382
305
|
});
|
|
383
|
-
const toolLoopResult = await runToolLoop(result, session, sessionId, send, log, buildAvailableTools(), recordUsage);
|
|
306
|
+
const toolLoopResult = await runToolLoop(result, session, sessionId, send, log, (0, utils_1.buildAvailableTools)(), recordUsage);
|
|
384
307
|
const toolLoopContent = toolLoopResult.content.trim();
|
|
385
308
|
const toolLoopHasShell = toolLoopContent.includes('<shell_script>');
|
|
386
309
|
const toolLoopHasFinal = toolLoopContent.includes('<final_answer>');
|
|
@@ -402,9 +325,9 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
402
325
|
// <final_answer>. The directive below tells it to use <shell_script> as
|
|
403
326
|
// a fallback instead of asking the user to run commands.
|
|
404
327
|
if (toolLoopResult.assistantMessage) {
|
|
405
|
-
|
|
328
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, toolLoopResult.assistantMessage);
|
|
406
329
|
}
|
|
407
|
-
|
|
330
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
408
331
|
role: 'user',
|
|
409
332
|
content: webToolFailed
|
|
410
333
|
? [
|
|
@@ -450,7 +373,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
450
373
|
turn: session.turns,
|
|
451
374
|
responseLength: result.content.length,
|
|
452
375
|
});
|
|
453
|
-
|
|
376
|
+
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
454
377
|
role: 'assistant',
|
|
455
378
|
content,
|
|
456
379
|
});
|
|
@@ -487,7 +410,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
487
410
|
subscriptionId: subscription.id,
|
|
488
411
|
turn: session.turns,
|
|
489
412
|
});
|
|
490
|
-
|
|
413
|
+
(0, utils_1.pushToSessionHistory)(log, session, { role: 'assistant', content });
|
|
491
414
|
send({
|
|
492
415
|
session_id: sessionId,
|
|
493
416
|
sender: 'agent',
|
|
@@ -499,64 +422,19 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
499
422
|
log.warn('Agent returned empty content with no recognized tags; sending error', {
|
|
500
423
|
sessionId,
|
|
501
424
|
});
|
|
502
|
-
sendFinalAnswer(send, sessionId, 'The agent returned an empty response. Please try again.', true);
|
|
425
|
+
(0, utils_1.sendFinalAnswer)(send, sessionId, 'The agent returned an empty response. Please try again.', true);
|
|
503
426
|
sessionMessages.delete(sessionId);
|
|
504
427
|
}
|
|
505
428
|
}
|
|
506
429
|
catch (err) {
|
|
507
430
|
log.error('Agent LLM call failed', { error: err });
|
|
508
431
|
const errorMessage = 'Agent failed to call language model. Please try again later.';
|
|
509
|
-
sendFinalAnswer(send, sessionId, errorMessage, true);
|
|
432
|
+
(0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
|
|
510
433
|
// Clear any cached session state so a subsequent attempt can
|
|
511
434
|
// start fresh without being polluted by a failed turn.
|
|
512
435
|
sessionMessages.delete(sessionId);
|
|
513
436
|
}
|
|
514
437
|
}
|
|
515
|
-
function sendFinalAnswer(send, sessionId, message, isError) {
|
|
516
|
-
send({
|
|
517
|
-
session_id: sessionId,
|
|
518
|
-
sender: 'agent',
|
|
519
|
-
content: `<final_answer>\n${message}\n</final_answer>`,
|
|
520
|
-
is_terminal_output: false,
|
|
521
|
-
is_error: isError,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
function createLazyAuthContext(authHeader, log) {
|
|
525
|
-
let authenticatedSubscription = null;
|
|
526
|
-
let authFailed = false;
|
|
527
|
-
let authPromise = null;
|
|
528
|
-
const ensureAuthenticated = async () => {
|
|
529
|
-
if (authenticatedSubscription) {
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
if (authFailed) {
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
if (!authPromise) {
|
|
536
|
-
authPromise = (async () => {
|
|
537
|
-
try {
|
|
538
|
-
const sub = await authenticateFromAuthHeader(authHeader, log);
|
|
539
|
-
if (!sub) {
|
|
540
|
-
authFailed = true;
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
authenticatedSubscription = sub;
|
|
544
|
-
log.info('Agent WebSocket authenticated', {
|
|
545
|
-
subscriptionId: authenticatedSubscription.id,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
catch (err) {
|
|
549
|
-
authFailed = true;
|
|
550
|
-
log.error('Unexpected error during agent WebSocket auth', { error: err });
|
|
551
|
-
}
|
|
552
|
-
})();
|
|
553
|
-
}
|
|
554
|
-
await authPromise;
|
|
555
|
-
return Boolean(authenticatedSubscription);
|
|
556
|
-
};
|
|
557
|
-
const getSubscription = () => authenticatedSubscription;
|
|
558
|
-
return { ensureAuthenticated, getSubscription };
|
|
559
|
-
}
|
|
560
438
|
function attachAgentWebSocketServer(server) {
|
|
561
439
|
const wss = new ws_1.WebSocketServer({ server, path: '/ws/omni-agent' });
|
|
562
440
|
wss.on('connection', (ws, req) => {
|
|
@@ -565,7 +443,7 @@ function attachAgentWebSocketServer(server) {
|
|
|
565
443
|
log.info('Agent WebSocket connection opened');
|
|
566
444
|
const authHeaderValue = req.headers['authorization'];
|
|
567
445
|
const authHeader = Array.isArray(authHeaderValue) ? authHeaderValue[0] : authHeaderValue;
|
|
568
|
-
const { ensureAuthenticated, getSubscription } = createLazyAuthContext(authHeader, log);
|
|
446
|
+
const { ensureAuthenticated, getSubscription } = (0, agentAuth_1.createLazyAuthContext)(authHeader, log);
|
|
569
447
|
const send = (msg) => {
|
|
570
448
|
try {
|
|
571
449
|
ws.send(JSON.stringify(msg));
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAvailableTools = buildAvailableTools;
|
|
4
|
+
exports.createUserContent = createUserContent;
|
|
5
|
+
exports.sendFinalAnswer = sendFinalAnswer;
|
|
6
|
+
exports.pushToSessionHistory = pushToSessionHistory;
|
|
7
|
+
const web_search_provider_1 = require("../web-search-provider");
|
|
8
|
+
const ai_client_1 = require("../ai-client");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
/**
|
|
11
|
+
* Returns the set of web tools available to the agent for every turn.
|
|
12
|
+
*
|
|
13
|
+
* `web_search` is always included because DuckDuckGo is used as a free
|
|
14
|
+
* fallback when no third-party search key is configured.
|
|
15
|
+
*
|
|
16
|
+
* @returns An array of `AITool` definitions ready to pass to the AI client.
|
|
17
|
+
*/
|
|
18
|
+
function buildAvailableTools() {
|
|
19
|
+
return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Strips the `@omniagent` mention from user-supplied content.
|
|
23
|
+
*
|
|
24
|
+
* The desktop client prefixes messages with `@omniAgent` to trigger the agent.
|
|
25
|
+
* This helper removes that prefix (case-insensitive) so the raw directive
|
|
26
|
+
* reaches the model without the routing annotation.
|
|
27
|
+
*
|
|
28
|
+
* @param content - Raw content string from the client message.
|
|
29
|
+
* @param hasStoredPrompt - only remove the mention if the command has a stored prompt, otherwise it may be part of the user input
|
|
30
|
+
* @returns The cleaned content string with the mention removed and whitespace trimmed.
|
|
31
|
+
*/
|
|
32
|
+
function createUserContent(content, hasStoredPrompt) {
|
|
33
|
+
if (hasStoredPrompt) {
|
|
34
|
+
return content.replace(/@omniagent/gi, '').trim();
|
|
35
|
+
}
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sends a `<final_answer>` message over the WebSocket and closes the agent turn.
|
|
40
|
+
*
|
|
41
|
+
* Wraps `message` in `<final_answer>` tags so the client knows the agent has
|
|
42
|
+
* finished reasoning and can display the result. Used for both successful
|
|
43
|
+
* conclusions and error responses.
|
|
44
|
+
*
|
|
45
|
+
* @param send - The WebSocket send function scoped to the current connection.
|
|
46
|
+
* @param sessionId - ID of the session this answer belongs to.
|
|
47
|
+
* @param message - The final answer text to send to the client.
|
|
48
|
+
* @param isError - When `true`, the client renders the message as an error.
|
|
49
|
+
*/
|
|
50
|
+
function sendFinalAnswer(send, sessionId, message, isError) {
|
|
51
|
+
send({
|
|
52
|
+
session_id: sessionId,
|
|
53
|
+
sender: 'agent',
|
|
54
|
+
content: `<final_answer>\n${message}\n</final_answer>`,
|
|
55
|
+
is_terminal_output: false,
|
|
56
|
+
is_error: isError,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Per-message hard string limit enforced by the provider API.
|
|
60
|
+
const MAX_MESSAGE_CONTENT = (0, ai_client_1.getMaxMessageContentLength)(config_1.config.aiProvider);
|
|
61
|
+
// Total character budget across all history messages (derived from the
|
|
62
|
+
// provider's context-window size minus headroom for output + system prompt).
|
|
63
|
+
const MAX_HISTORY_TOTAL = (0, ai_client_1.getMaxHistoryLength)(config_1.config.aiProvider);
|
|
64
|
+
const FINAL_ANSWER_REQUEST = {
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: 'Content was truncated because a length limit was reached. ' +
|
|
67
|
+
'You MUST stop making tool calls and provide a final answer immediately using <final_answer>...</final_answer>.',
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Pushes a message onto the session history, enforcing two independent limits:
|
|
71
|
+
*
|
|
72
|
+
* 1. **Per-message limit** (`MAX_MESSAGE_CONTENT`) — the provider's hard cap
|
|
73
|
+
* on a single content string (e.g. Anthropic: 10 MB, OpenAI/Gemini: context-bound).
|
|
74
|
+
* 2. **Total history limit** (`MAX_HISTORY_TOTAL`) — the cumulative character
|
|
75
|
+
* budget derived from each provider's context-window size.
|
|
76
|
+
*
|
|
77
|
+
* When either limit is hit the message content is truncated and a separate
|
|
78
|
+
* `user` message is appended instructing the model to emit a final answer.
|
|
79
|
+
*/
|
|
80
|
+
function pushToSessionHistory(logger, session, message) {
|
|
81
|
+
if (typeof message.content !== 'string') {
|
|
82
|
+
session.history.push(message);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let content = message.content;
|
|
86
|
+
let limitHit = false;
|
|
87
|
+
// 1. Per-message content limit.
|
|
88
|
+
if (content.length > MAX_MESSAGE_CONTENT) {
|
|
89
|
+
content = content.slice(0, MAX_MESSAGE_CONTENT);
|
|
90
|
+
limitHit = true;
|
|
91
|
+
}
|
|
92
|
+
// 2. Total history length limit.
|
|
93
|
+
const currentTotal = session.history.reduce((acc, msg) => acc + (typeof msg.content === 'string' ? msg.content.length : 0), 0);
|
|
94
|
+
const remaining = MAX_HISTORY_TOTAL - currentTotal;
|
|
95
|
+
if (content.length > remaining) {
|
|
96
|
+
content = content.slice(0, Math.max(0, remaining - FINAL_ANSWER_REQUEST.content.length));
|
|
97
|
+
limitHit = true;
|
|
98
|
+
}
|
|
99
|
+
session.history.push({ ...message, content });
|
|
100
|
+
if (limitHit) {
|
|
101
|
+
logger.warn(`History limits exceeded. Message truncated to ${content.length} chars, total history is now ${currentTotal + content.length} chars.`);
|
|
102
|
+
session.history.push(FINAL_ANSWER_REQUEST);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.aiClient = exports.AIClient = void 0;
|
|
7
7
|
exports.getDefaultModel = getDefaultModel;
|
|
8
|
+
exports.getMaxMessageContentLength = getMaxMessageContentLength;
|
|
9
|
+
exports.getMaxHistoryLength = getMaxHistoryLength;
|
|
8
10
|
const openai_1 = __importDefault(require("openai"));
|
|
9
11
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
10
12
|
const genai_1 = require("@google/genai");
|
|
@@ -21,6 +23,44 @@ const DEFAULT_MODELS = {
|
|
|
21
23
|
function getDefaultModel(provider, tier) {
|
|
22
24
|
return DEFAULT_MODELS[provider][tier];
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Maximum character length for a single message content string per provider.
|
|
28
|
+
*
|
|
29
|
+
* - anthropic: hard API-enforced string limit of 10,485,760 chars; we stay
|
|
30
|
+
* just below it with a small safety buffer.
|
|
31
|
+
* - openai: no documented per-string limit; bounded by the context window
|
|
32
|
+
* (~272K tokens for GPT-5.1 ≈ ~1M chars). Use the history cap.
|
|
33
|
+
* - gemini: no documented per-string limit; bounded by the 1M-token
|
|
34
|
+
* context window (~4M chars). Use the history cap.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER = {
|
|
37
|
+
anthropic: 10000000,
|
|
38
|
+
openai: 800000,
|
|
39
|
+
gemini: 3500000,
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Maximum total character length across all messages in the conversation
|
|
43
|
+
* history, derived from each provider's context-window size minus headroom
|
|
44
|
+
* for the system prompt and max output tokens.
|
|
45
|
+
*
|
|
46
|
+
* - anthropic: Claude Sonnet 4.6 — 1M token ctx, 64K max output
|
|
47
|
+
* ≈ (1,000,000 - 64,000 - 10,000) tokens × 4 chars ≈ 3.7M chars
|
|
48
|
+
* - openai: GPT-5.1 — ~272K token ctx, ~32K max output
|
|
49
|
+
* ≈ (272,000 - 32,000 - 5,000) tokens × 4 chars ≈ 940K chars
|
|
50
|
+
* - gemini: Gemini 2.5 Pro — 1M token ctx, ~32K max output
|
|
51
|
+
* ≈ (1,000,000 - 32,000 - 10,000) tokens × 4 chars ≈ 3.8M chars
|
|
52
|
+
*/
|
|
53
|
+
const MAX_HISTORY_LENGTH_BY_PROVIDER = {
|
|
54
|
+
anthropic: 3500000,
|
|
55
|
+
openai: 800000,
|
|
56
|
+
gemini: 3500000,
|
|
57
|
+
};
|
|
58
|
+
function getMaxMessageContentLength(provider) {
|
|
59
|
+
return MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER[provider];
|
|
60
|
+
}
|
|
61
|
+
function getMaxHistoryLength(provider) {
|
|
62
|
+
return MAX_HISTORY_LENGTH_BY_PROVIDER[provider];
|
|
63
|
+
}
|
|
24
64
|
// ---------------------------------------------------------------------------
|
|
25
65
|
// OpenAI adapter
|
|
26
66
|
// ---------------------------------------------------------------------------
|
package/backend-dist/config.js
CHANGED
|
@@ -91,5 +91,5 @@ exports.config = {
|
|
|
91
91
|
braveSearchApiKey: getEnv('BRAVE_SEARCH_API_KEY', false),
|
|
92
92
|
tavilyApiKey: getEnv('TAVILY_API_KEY', false),
|
|
93
93
|
searxngUrl: getEnv('SEARXNG_URL', false),
|
|
94
|
-
terminalPlatform: getEnv('TERMINAL_PLATFORM', false)
|
|
94
|
+
terminalPlatform: getEnv('TERMINAL_PLATFORM', false),
|
|
95
95
|
};
|
package/backend-dist/index.js
CHANGED
|
@@ -15,8 +15,10 @@ const logger_1 = require("./logger");
|
|
|
15
15
|
const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
|
|
16
16
|
const config_1 = require("./config");
|
|
17
17
|
const agentServer_1 = require("./agent/agentServer");
|
|
18
|
+
const appDownload_1 = require("./models/appDownload");
|
|
18
19
|
const app = (0, express_1.default)();
|
|
19
20
|
const PORT = Number(config_1.config.port);
|
|
21
|
+
app.set('trust proxy', 1);
|
|
20
22
|
app.use((0, cors_1.default)());
|
|
21
23
|
app.use(express_1.default.json());
|
|
22
24
|
// Landing page
|
|
@@ -30,6 +32,14 @@ app.get('/macos/download', (_req, res) => {
|
|
|
30
32
|
res.status(404).send('File not found.');
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
35
|
+
if (!config_1.config.isSelfHosted) {
|
|
36
|
+
appDownload_1.AppDownload.findOrCreate({
|
|
37
|
+
where: { platform: 'macos' },
|
|
38
|
+
defaults: { platform: 'macos', count: 0 },
|
|
39
|
+
})
|
|
40
|
+
.then(([record]) => record.increment('count'))
|
|
41
|
+
.catch((err) => logger_1.logger.error('Failed to increment macOS download count.', { error: err }));
|
|
42
|
+
}
|
|
33
43
|
res.set({
|
|
34
44
|
'Content-Type': 'application/octet-stream',
|
|
35
45
|
'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
|
|
@@ -59,13 +69,13 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
59
69
|
catch (error) {
|
|
60
70
|
logger_1.logger.error('Failed to stat OmniKeyAI.dmg for appcast.', { error });
|
|
61
71
|
}
|
|
62
|
-
const baseUrl =
|
|
72
|
+
const baseUrl = `https://${req.get('host')}`;
|
|
63
73
|
const downloadUrl = `${baseUrl}/macos/download`;
|
|
64
74
|
const appcastUrl = `${baseUrl}/macos/appcast`;
|
|
65
75
|
// These should match the values embedded into the macOS app
|
|
66
76
|
// Info.plist in macOS/build_release_dmg.sh.
|
|
67
|
-
const bundleVersion = '
|
|
68
|
-
const shortVersion = '1.0.
|
|
77
|
+
const bundleVersion = '19';
|
|
78
|
+
const shortVersion = '1.0.18';
|
|
69
79
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
|
70
80
|
<rss version="2.0"
|
|
71
81
|
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
|
@@ -93,7 +103,7 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
93
103
|
// ── Windows distribution endpoints ───────────────────────────────────────────
|
|
94
104
|
// These should match the values in windows/OmniKey.Windows.csproj
|
|
95
105
|
// <Version> and windows/build_release_zip.ps1 $APP_VERSION.
|
|
96
|
-
const WIN_VERSION = '1.
|
|
106
|
+
const WIN_VERSION = '1.7';
|
|
97
107
|
const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
|
|
98
108
|
const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
|
|
99
109
|
// Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
|
|
@@ -103,6 +113,14 @@ app.get('/windows/download', (_req, res) => {
|
|
|
103
113
|
res.status(404).send('File not found.');
|
|
104
114
|
return;
|
|
105
115
|
}
|
|
116
|
+
if (!config_1.config.isSelfHosted) {
|
|
117
|
+
appDownload_1.AppDownload.findOrCreate({
|
|
118
|
+
where: { platform: 'windows' },
|
|
119
|
+
defaults: { platform: 'windows', count: 0 },
|
|
120
|
+
})
|
|
121
|
+
.then(([record]) => record.increment('count'))
|
|
122
|
+
.catch((err) => logger_1.logger.error('Failed to increment Windows download count.', { error: err }));
|
|
123
|
+
}
|
|
106
124
|
res.set({
|
|
107
125
|
'Content-Type': 'application/zip',
|
|
108
126
|
'Content-Disposition': `attachment; filename="${WIN_ZIP_FILENAME}"`,
|
|
@@ -122,7 +140,7 @@ app.get('/windows/download', (_req, res) => {
|
|
|
122
140
|
// Returns the latest version + download URL so the client can decide whether
|
|
123
141
|
// to prompt the user for an update.
|
|
124
142
|
app.get('/windows/update', (req, res) => {
|
|
125
|
-
const baseUrl =
|
|
143
|
+
const baseUrl = `https://${req.get('host')}`;
|
|
126
144
|
let fileSize = 0;
|
|
127
145
|
try {
|
|
128
146
|
fileSize = fs_1.default.statSync(WIN_ZIP_PATH).size;
|
|
@@ -137,6 +155,11 @@ app.get('/windows/update', (req, res) => {
|
|
|
137
155
|
releaseNotes: '',
|
|
138
156
|
});
|
|
139
157
|
});
|
|
158
|
+
app.get('/api/downloads', async (_req, res) => {
|
|
159
|
+
const rows = await appDownload_1.AppDownload.findAll({ where: { platform: ['macos', 'windows'] } });
|
|
160
|
+
const find = (p) => Number(rows.find((r) => r.platform === p)?.count ?? 0);
|
|
161
|
+
res.json({ macos: find('macos'), windows: find('windows') });
|
|
162
|
+
});
|
|
140
163
|
app.get('/health', (_req, res) => {
|
|
141
164
|
res.json({ status: 'ok' });
|
|
142
165
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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.AppDownload = void 0;
|
|
7
|
+
const sequelize_1 = require("sequelize");
|
|
8
|
+
const cuid_1 = __importDefault(require("cuid"));
|
|
9
|
+
const db_1 = require("../db");
|
|
10
|
+
class AppDownload extends sequelize_1.Model {
|
|
11
|
+
}
|
|
12
|
+
exports.AppDownload = AppDownload;
|
|
13
|
+
AppDownload.init({
|
|
14
|
+
id: {
|
|
15
|
+
type: sequelize_1.DataTypes.STRING,
|
|
16
|
+
primaryKey: true,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
defaultValue: () => (0, cuid_1.default)(),
|
|
19
|
+
},
|
|
20
|
+
platform: {
|
|
21
|
+
type: sequelize_1.DataTypes.STRING,
|
|
22
|
+
allowNull: false,
|
|
23
|
+
unique: true,
|
|
24
|
+
},
|
|
25
|
+
count: {
|
|
26
|
+
type: sequelize_1.DataTypes.BIGINT,
|
|
27
|
+
allowNull: false,
|
|
28
|
+
defaultValue: 0,
|
|
29
|
+
},
|
|
30
|
+
}, {
|
|
31
|
+
sequelize: db_1.sequelize,
|
|
32
|
+
tableName: 'app_downloads',
|
|
33
|
+
modelName: 'AppDownload',
|
|
34
|
+
});
|
|
@@ -157,7 +157,10 @@ async function executeTool(name, args, log) {
|
|
|
157
157
|
return text || 'No content retrieved';
|
|
158
158
|
}
|
|
159
159
|
catch (err) {
|
|
160
|
-
log.warn('web_fetch tool failed', {
|
|
160
|
+
log.warn('web_fetch tool failed', {
|
|
161
|
+
url,
|
|
162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
163
|
+
});
|
|
161
164
|
return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`;
|
|
162
165
|
}
|
|
163
166
|
}
|
|
@@ -170,7 +173,10 @@ async function executeTool(name, args, log) {
|
|
|
170
173
|
return await executeWebSearch(query, log);
|
|
171
174
|
}
|
|
172
175
|
catch (err) {
|
|
173
|
-
log.warn('web_search tool failed', {
|
|
176
|
+
log.warn('web_search tool failed', {
|
|
177
|
+
query,
|
|
178
|
+
error: err instanceof Error ? err.message : String(err),
|
|
179
|
+
});
|
|
174
180
|
return `Error searching: ${err instanceof Error ? err.message : String(err)}`;
|
|
175
181
|
}
|
|
176
182
|
}
|
package/dist/daemon.js
CHANGED
|
@@ -87,7 +87,10 @@ async function startDaemonWindows(opts) {
|
|
|
87
87
|
// won't see it — spawn a fresh cmd to resolve the new location.
|
|
88
88
|
try {
|
|
89
89
|
nssmPath = (0, child_process_1.execSync)('cmd /c where nssm', { stdio: 'pipe' })
|
|
90
|
-
.toString()
|
|
90
|
+
.toString()
|
|
91
|
+
.trim()
|
|
92
|
+
.split('\n')[0]
|
|
93
|
+
.trim();
|
|
91
94
|
}
|
|
92
95
|
catch {
|
|
93
96
|
nssmPath = null;
|
|
@@ -103,11 +106,15 @@ async function startDaemonWindows(opts) {
|
|
|
103
106
|
try {
|
|
104
107
|
(0, child_process_1.execFileSync)(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
105
108
|
}
|
|
106
|
-
catch {
|
|
109
|
+
catch {
|
|
110
|
+
/* not running */
|
|
111
|
+
}
|
|
107
112
|
try {
|
|
108
113
|
(0, child_process_1.execFileSync)(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
109
114
|
}
|
|
110
|
-
catch {
|
|
115
|
+
catch {
|
|
116
|
+
/* didn't exist */
|
|
117
|
+
}
|
|
111
118
|
// NSSM services run as LocalSystem; pass USERPROFILE so the backend's
|
|
112
119
|
// getHomeDir() resolves to the correct user config directory.
|
|
113
120
|
const env = {
|
|
@@ -122,17 +129,25 @@ async function startDaemonWindows(opts) {
|
|
|
122
129
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppDirectory', configDir], { stdio: 'pipe' });
|
|
123
130
|
// Pass all env vars in a single call (replaces the entire AppEnvironmentExtra key)
|
|
124
131
|
const envEntries = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
125
|
-
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], {
|
|
132
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], {
|
|
133
|
+
stdio: 'pipe',
|
|
134
|
+
});
|
|
126
135
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppStdout', logPath], { stdio: 'pipe' });
|
|
127
136
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppStderr', errorLogPath], { stdio: 'pipe' });
|
|
128
137
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppRotateFiles', '1'], { stdio: 'pipe' });
|
|
129
138
|
// Restart automatically after a 3-second delay on any exit
|
|
130
|
-
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], {
|
|
139
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], {
|
|
140
|
+
stdio: 'pipe',
|
|
141
|
+
});
|
|
131
142
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppRestartDelay', '3000'], { stdio: 'pipe' });
|
|
132
143
|
// Start automatically at boot (no login required)
|
|
133
144
|
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'Start', 'SERVICE_AUTO_START'], { stdio: 'pipe' });
|
|
134
|
-
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], {
|
|
135
|
-
|
|
145
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], {
|
|
146
|
+
stdio: 'pipe',
|
|
147
|
+
});
|
|
148
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], {
|
|
149
|
+
stdio: 'pipe',
|
|
150
|
+
});
|
|
136
151
|
(0, child_process_1.execFileSync)(nssmPath, ['start', serviceName], { stdio: 'pipe' });
|
|
137
152
|
console.log(`NSSM service installed and started: ${serviceName}`);
|
|
138
153
|
console.log('Omnikey daemon runs on boot, without login, and auto-restarts on crash.');
|
package/dist/removeConfig.js
CHANGED
|
@@ -35,12 +35,16 @@ function killWindowsTask() {
|
|
|
35
35
|
try {
|
|
36
36
|
nssmPath = (0, child_process_1.execSync)('where nssm', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
37
37
|
}
|
|
38
|
-
catch {
|
|
38
|
+
catch {
|
|
39
|
+
/* NSSM not installed */
|
|
40
|
+
}
|
|
39
41
|
if (nssmPath) {
|
|
40
42
|
try {
|
|
41
43
|
(0, child_process_1.execFileSync)(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
42
44
|
}
|
|
43
|
-
catch {
|
|
45
|
+
catch {
|
|
46
|
+
/* not running */
|
|
47
|
+
}
|
|
44
48
|
try {
|
|
45
49
|
(0, child_process_1.execFileSync)(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
46
50
|
console.log(`Removed NSSM service: ${serviceName}`);
|
|
@@ -54,7 +58,9 @@ function killWindowsTask() {
|
|
|
54
58
|
try {
|
|
55
59
|
(0, child_process_1.execSync)(`schtasks /end /tn "${serviceName}"`, { stdio: 'pipe' });
|
|
56
60
|
}
|
|
57
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
/* not running */
|
|
63
|
+
}
|
|
58
64
|
try {
|
|
59
65
|
(0, child_process_1.execSync)(`schtasks /delete /tn "${serviceName}" /f`, { stdio: 'pipe' });
|
|
60
66
|
console.log(`Removed Windows Task Scheduler task: ${serviceName}`);
|
|
@@ -69,7 +75,9 @@ function killWindowsTask() {
|
|
|
69
75
|
try {
|
|
70
76
|
fs_1.default.rmSync(wrapperPath);
|
|
71
77
|
}
|
|
72
|
-
catch {
|
|
78
|
+
catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
/**
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.24",
|
|
8
8
|
"description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=14.0.0",
|
package/src/daemon.ts
CHANGED
|
@@ -86,7 +86,9 @@ async function startDaemonWindows(opts: DaemonOptions) {
|
|
|
86
86
|
]);
|
|
87
87
|
|
|
88
88
|
if (!install) {
|
|
89
|
-
console.log(
|
|
89
|
+
console.log(
|
|
90
|
+
'Aborted. Install NSSM manually and re-run in an elevated (Administrator) terminal.',
|
|
91
|
+
);
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -105,7 +107,10 @@ async function startDaemonWindows(opts: DaemonOptions) {
|
|
|
105
107
|
// won't see it — spawn a fresh cmd to resolve the new location.
|
|
106
108
|
try {
|
|
107
109
|
nssmPath = execSync('cmd /c where nssm', { stdio: 'pipe' })
|
|
108
|
-
.toString()
|
|
110
|
+
.toString()
|
|
111
|
+
.trim()
|
|
112
|
+
.split('\n')[0]
|
|
113
|
+
.trim();
|
|
109
114
|
} catch {
|
|
110
115
|
nssmPath = null;
|
|
111
116
|
}
|
|
@@ -120,8 +125,16 @@ async function startDaemonWindows(opts: DaemonOptions) {
|
|
|
120
125
|
initLogFiles(logPath, errorLogPath);
|
|
121
126
|
|
|
122
127
|
// Remove any existing service (stop first, then remove)
|
|
123
|
-
try {
|
|
124
|
-
|
|
128
|
+
try {
|
|
129
|
+
execFileSync(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
130
|
+
} catch {
|
|
131
|
+
/* not running */
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
execFileSync(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
135
|
+
} catch {
|
|
136
|
+
/* didn't exist */
|
|
137
|
+
}
|
|
125
138
|
|
|
126
139
|
// NSSM services run as LocalSystem; pass USERPROFILE so the backend's
|
|
127
140
|
// getHomeDir() resolves to the correct user config directory.
|
|
@@ -140,21 +153,29 @@ async function startDaemonWindows(opts: DaemonOptions) {
|
|
|
140
153
|
|
|
141
154
|
// Pass all env vars in a single call (replaces the entire AppEnvironmentExtra key)
|
|
142
155
|
const envEntries = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
143
|
-
execFileSync(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], {
|
|
156
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], {
|
|
157
|
+
stdio: 'pipe',
|
|
158
|
+
});
|
|
144
159
|
|
|
145
160
|
execFileSync(nssmPath, ['set', serviceName, 'AppStdout', logPath], { stdio: 'pipe' });
|
|
146
161
|
execFileSync(nssmPath, ['set', serviceName, 'AppStderr', errorLogPath], { stdio: 'pipe' });
|
|
147
162
|
execFileSync(nssmPath, ['set', serviceName, 'AppRotateFiles', '1'], { stdio: 'pipe' });
|
|
148
163
|
|
|
149
164
|
// Restart automatically after a 3-second delay on any exit
|
|
150
|
-
execFileSync(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], {
|
|
165
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], {
|
|
166
|
+
stdio: 'pipe',
|
|
167
|
+
});
|
|
151
168
|
execFileSync(nssmPath, ['set', serviceName, 'AppRestartDelay', '3000'], { stdio: 'pipe' });
|
|
152
169
|
|
|
153
170
|
// Start automatically at boot (no login required)
|
|
154
171
|
execFileSync(nssmPath, ['set', serviceName, 'Start', 'SERVICE_AUTO_START'], { stdio: 'pipe' });
|
|
155
172
|
|
|
156
|
-
execFileSync(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], {
|
|
157
|
-
|
|
173
|
+
execFileSync(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], {
|
|
174
|
+
stdio: 'pipe',
|
|
175
|
+
});
|
|
176
|
+
execFileSync(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], {
|
|
177
|
+
stdio: 'pipe',
|
|
178
|
+
});
|
|
158
179
|
|
|
159
180
|
execFileSync(nssmPath, ['start', serviceName], { stdio: 'pipe' });
|
|
160
181
|
|
package/src/removeConfig.ts
CHANGED
|
@@ -26,10 +26,16 @@ export function killWindowsTask() {
|
|
|
26
26
|
let nssmPath: string | null = null;
|
|
27
27
|
try {
|
|
28
28
|
nssmPath = execSync('where nssm', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
29
|
-
} catch {
|
|
29
|
+
} catch {
|
|
30
|
+
/* NSSM not installed */
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
if (nssmPath) {
|
|
32
|
-
try {
|
|
34
|
+
try {
|
|
35
|
+
execFileSync(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
36
|
+
} catch {
|
|
37
|
+
/* not running */
|
|
38
|
+
}
|
|
33
39
|
try {
|
|
34
40
|
execFileSync(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
35
41
|
console.log(`Removed NSSM service: ${serviceName}`);
|
|
@@ -38,7 +44,11 @@ export function killWindowsTask() {
|
|
|
38
44
|
}
|
|
39
45
|
} else {
|
|
40
46
|
// Fallback: remove legacy Task Scheduler task from previous installs
|
|
41
|
-
try {
|
|
47
|
+
try {
|
|
48
|
+
execSync(`schtasks /end /tn "${serviceName}"`, { stdio: 'pipe' });
|
|
49
|
+
} catch {
|
|
50
|
+
/* not running */
|
|
51
|
+
}
|
|
42
52
|
try {
|
|
43
53
|
execSync(`schtasks /delete /tn "${serviceName}" /f`, { stdio: 'pipe' });
|
|
44
54
|
console.log(`Removed Windows Task Scheduler task: ${serviceName}`);
|
|
@@ -50,7 +60,11 @@ export function killWindowsTask() {
|
|
|
50
60
|
// Remove legacy wrapper script if present
|
|
51
61
|
const wrapperPath = path.join(getConfigDir(), 'start-daemon.cmd');
|
|
52
62
|
if (fs.existsSync(wrapperPath)) {
|
|
53
|
-
try {
|
|
63
|
+
try {
|
|
64
|
+
fs.rmSync(wrapperPath);
|
|
65
|
+
} catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|