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.
@@ -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
- session.history.push(result.assistantMessage);
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
- session.history.push({
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
- session.history.push(result.assistantMessage);
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
- session.history.push({
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
- session.history.push({
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
- session.history.push({
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
- session.history.push({
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
- session.history.push(toolLoopResult.assistantMessage);
328
+ (0, utils_1.pushToSessionHistory)(logger_1.logger, session, toolLoopResult.assistantMessage);
406
329
  }
407
- session.history.push({
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
- session.history.push({
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
- session.history.push({ role: 'assistant', content });
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
  // ---------------------------------------------------------------------------
@@ -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
  };
@@ -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 = `${req.protocol}://${req.get('host')}`;
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 = '18';
68
- const shortVersion = '1.0.17';
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.4';
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 = `${req.protocol}://${req.get('host')}`;
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', { url, error: err });
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', { query, error: err });
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().trim().split('\n')[0].trim();
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 { /* not running */ }
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 { /* didn't exist */ }
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], { stdio: 'pipe' });
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'], { stdio: 'pipe' });
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'], { stdio: 'pipe' });
135
- (0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], { stdio: 'pipe' });
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.');
@@ -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 { /* NSSM not installed */ }
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 { /* not running */ }
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 { /* not running */ }
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 { /* ignore */ }
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.22",
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('Aborted. Install NSSM manually and re-run in an elevated (Administrator) terminal.');
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().trim().split('\n')[0].trim();
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 { execFileSync(nssmPath, ['stop', serviceName], { stdio: 'pipe' }); } catch { /* not running */ }
124
- try { execFileSync(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' }); } catch { /* didn't exist */ }
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], { stdio: 'pipe' });
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'], { stdio: 'pipe' });
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'], { stdio: 'pipe' });
157
- execFileSync(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], { stdio: 'pipe' });
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
 
@@ -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 { /* NSSM not installed */ }
29
+ } catch {
30
+ /* NSSM not installed */
31
+ }
30
32
 
31
33
  if (nssmPath) {
32
- try { execFileSync(nssmPath, ['stop', serviceName], { stdio: 'pipe' }); } catch { /* not running */ }
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 { execSync(`schtasks /end /tn "${serviceName}"`, { stdio: 'pipe' }); } catch { /* not running */ }
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 { fs.rmSync(wrapperPath); } catch { /* ignore */ }
63
+ try {
64
+ fs.rmSync(wrapperPath);
65
+ } catch {
66
+ /* ignore */
67
+ }
54
68
  }
55
69
  }
56
70