omnikey-cli 1.0.23 → 1.0.25
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 +21 -143
- 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 +25 -3
- package/backend-dist/models/appDownload.js +34 -0
- package/backend-dist/web-search/browser-playwright.js +613 -0
- package/backend-dist/web-search/index.js +17 -0
- package/backend-dist/web-search/llm-auth-check.js +127 -0
- package/backend-dist/{web-search-provider.js → web-search/web-search-provider.js} +106 -18
- package/dist/daemon.js +22 -7
- package/dist/removeConfig.js +12 -4
- package/package.json +3 -2
- package/src/daemon.ts +29 -8
- package/src/removeConfig.ts +18 -4
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPageAuthenticated = isPageAuthenticated;
|
|
4
|
+
const ai_client_1 = require("../ai-client");
|
|
5
|
+
const ai_client_2 = require("../ai-client");
|
|
6
|
+
const SYSTEM_PROMPT = 'You are an expert at detecting whether a web page is showing the real requested content. ' +
|
|
7
|
+
'Given a URL and the visible text content of a web page, answer "yes" if EITHER: ' +
|
|
8
|
+
'(1) The URL looks like a public resource that does not require authentication — such as documentation sites, ' +
|
|
9
|
+
'public wikis, news articles, open-source repos, package registries, developer references, or any URL whose ' +
|
|
10
|
+
'hostname/path strongly suggests publicly accessible content (e.g. docs.*, developer.*, wikipedia.org, github.com public repos, ' +
|
|
11
|
+
'stackoverflow.com, npmjs.com, medium.com, reddit.com, youtube.com, etc.). ' +
|
|
12
|
+
'(2) The page is showing the actual content that an authenticated user would see at that URL. ' +
|
|
13
|
+
'Answer "no" if the page is: a login/sign-in page, an access denied or unauthorized page, a redirect away from the requested URL, ' +
|
|
14
|
+
'a generic 404/not-found or error page that could be an auth redirect in disguise (e.g. shows a not-found message but ' +
|
|
15
|
+
'the URL was a valid authenticated route), or any page that does not correspond to the requested resource. ' +
|
|
16
|
+
'When in doubt about whether a URL is public, lean towards "yes". Reply with only one word: "yes" or "no".';
|
|
17
|
+
const PUBLIC_URL_PATTERNS = [
|
|
18
|
+
/^https?:\/\/(www\.)?github\.com\/(?!.*\/settings|.*\/account)/,
|
|
19
|
+
/^https?:\/\/(www\.)?stackoverflow\.com/,
|
|
20
|
+
/^https?:\/\/(www\.)?wikipedia\.org/,
|
|
21
|
+
/^https?:\/\/docs\./,
|
|
22
|
+
/^https?:\/\/developer\./,
|
|
23
|
+
/^https?:\/\/(www\.)?npmjs\.com/,
|
|
24
|
+
/^https?:\/\/(www\.)?pypi\.org/,
|
|
25
|
+
/^https?:\/\/(www\.)?medium\.com/,
|
|
26
|
+
/^https?:\/\/(www\.)?reddit\.com/,
|
|
27
|
+
/^https?:\/\/(www\.)?youtube\.com/,
|
|
28
|
+
/^https?:\/\/(www\.)?news\.ycombinator\.com/,
|
|
29
|
+
// Package registries & language docs
|
|
30
|
+
/^https?:\/\/(www\.)?crates\.io/,
|
|
31
|
+
/^https?:\/\/(www\.)?rubygems\.org/,
|
|
32
|
+
/^https?:\/\/(www\.)?packagist\.org/,
|
|
33
|
+
/^https?:\/\/(www\.)?pkg\.go\.dev/,
|
|
34
|
+
/^https?:\/\/(www\.)?hex\.pm/,
|
|
35
|
+
/^https?:\/\/(www\.)?nuget\.org/,
|
|
36
|
+
/^https?:\/\/(www\.)?maven\.apache\.org/,
|
|
37
|
+
/^https?:\/\/central\.sonatype\.com/,
|
|
38
|
+
// Official language & runtime docs
|
|
39
|
+
/^https?:\/\/(www\.)?python\.org/,
|
|
40
|
+
/^https?:\/\/(www\.)?rust-lang\.org/,
|
|
41
|
+
/^https?:\/\/(www\.)?golang\.org/,
|
|
42
|
+
/^https?:\/\/(www\.)?go\.dev/,
|
|
43
|
+
/^https?:\/\/(www\.)?ruby-lang\.org/,
|
|
44
|
+
/^https?:\/\/(www\.)?php\.net/,
|
|
45
|
+
/^https?:\/\/(www\.)?kotlinlang\.org/,
|
|
46
|
+
/^https?:\/\/(www\.)?swift\.org/,
|
|
47
|
+
/^https?:\/\/learn\.microsoft\.com/,
|
|
48
|
+
/^https?:\/\/msdn\.microsoft\.com/,
|
|
49
|
+
/^https?:\/\/devblogs\.microsoft\.com/,
|
|
50
|
+
/^https?:\/\/(www\.)?w3\.org/,
|
|
51
|
+
/^https?:\/\/(www\.)?w3schools\.com/,
|
|
52
|
+
/^https?:\/\/(www\.)?mdn\./,
|
|
53
|
+
/^https?:\/\/developer\.mozilla\.org/,
|
|
54
|
+
// Source code & open-source platforms
|
|
55
|
+
/^https?:\/\/(www\.)?gitlab\.com\/(?!.*\/-\/settings)/,
|
|
56
|
+
/^https?:\/\/(www\.)?bitbucket\.org\/(?!.*\/admin)/,
|
|
57
|
+
/^https?:\/\/(www\.)?sourceforge\.net/,
|
|
58
|
+
/^https?:\/\/(www\.)?codepen\.io/,
|
|
59
|
+
/^https?:\/\/(www\.)?jsfiddle\.net/,
|
|
60
|
+
/^https?:\/\/(www\.)?codesandbox\.io/,
|
|
61
|
+
// Q&A, forums & community sites
|
|
62
|
+
/^https?:\/\/(www\.)?stackexchange\.com/,
|
|
63
|
+
/^https?:\/\/(www\.)?superuser\.com/,
|
|
64
|
+
/^https?:\/\/(www\.)?serverfault\.com/,
|
|
65
|
+
/^https?:\/\/(www\.)?askubuntu\.com/,
|
|
66
|
+
/^https?:\/\/(www\.)?quora\.com/,
|
|
67
|
+
/^https?:\/\/(www\.)?dev\.to/,
|
|
68
|
+
/^https?:\/\/(www\.)?hashnode\.com/,
|
|
69
|
+
/^https?:\/\/(www\.)?lobste\.rs/,
|
|
70
|
+
// News & tech media
|
|
71
|
+
/^https?:\/\/(www\.)?techcrunch\.com/,
|
|
72
|
+
/^https?:\/\/(www\.)?theverge\.com/,
|
|
73
|
+
/^https?:\/\/(www\.)?wired\.com/,
|
|
74
|
+
/^https?:\/\/(www\.)?arstechnica\.com/,
|
|
75
|
+
/^https?:\/\/(www\.)?thenextweb\.com/,
|
|
76
|
+
/^https?:\/\/(www\.)?infoq\.com/,
|
|
77
|
+
/^https?:\/\/(www\.)?smashingmagazine\.com/,
|
|
78
|
+
/^https?:\/\/(www\.)?css-tricks\.com/,
|
|
79
|
+
// Reference & encyclopedias
|
|
80
|
+
/^https?:\/\/[a-z-]+\.wikipedia\.org/,
|
|
81
|
+
/^https?:\/\/(www\.)?wikidata\.org/,
|
|
82
|
+
/^https?:\/\/(www\.)?wikimedia\.org/,
|
|
83
|
+
/^https?:\/\/(www\.)?archive\.org/,
|
|
84
|
+
// Cloud provider public docs
|
|
85
|
+
/^https?:\/\/cloud\.google\.com\/(?!.*\/console)/,
|
|
86
|
+
/^https?:\/\/aws\.amazon\.com\/(?!(.*\/console|.*\/signin))/,
|
|
87
|
+
/^https?:\/\/(www\.)?azure\.microsoft\.com/,
|
|
88
|
+
/^https?:\/\/registry\./,
|
|
89
|
+
];
|
|
90
|
+
const AUTH_PATH_PATTERN = /[/?#](login|log-in|signin|sign-in|signup|sign-up|register|auth|authenticate|oauth|sso|saml|forgot-password|reset-password|verify|two-factor|2fa|mfa)([/?#]|$)/i;
|
|
91
|
+
function isPublicUrl(url) {
|
|
92
|
+
if (AUTH_PATH_PATTERN.test(url))
|
|
93
|
+
return false;
|
|
94
|
+
return PUBLIC_URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
95
|
+
}
|
|
96
|
+
async function isPageAuthenticated(content, url, log, finalUrl) {
|
|
97
|
+
if (finalUrl) {
|
|
98
|
+
const normalize = (u) => u.replace(/#.*$/, '').replace(/\/$/, '');
|
|
99
|
+
if (normalize(finalUrl) !== normalize(url)) {
|
|
100
|
+
log.info('llm-auth-check: redirect detected, treating as not authenticated', {
|
|
101
|
+
requestUrl: url,
|
|
102
|
+
finalUrl,
|
|
103
|
+
});
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (isPublicUrl(url)) {
|
|
108
|
+
log.info('llm-auth-check: public URL, skipping auth check', { url });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const model = (0, ai_client_2.getDefaultModel)(ai_client_1.aiClient.getProvider(), 'fast');
|
|
112
|
+
const messages = [
|
|
113
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
114
|
+
{ role: 'user', content: `URL: ${url}\n\nPage content:\n${content}` },
|
|
115
|
+
];
|
|
116
|
+
try {
|
|
117
|
+
const result = await ai_client_1.aiClient.complete(model, messages, { temperature: 0, maxTokens: 1 });
|
|
118
|
+
const answer = result.content.trim().toLowerCase();
|
|
119
|
+
log.info('llm-auth-check: LLM response', { url, answer });
|
|
120
|
+
return answer === 'yes';
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
log.error('llm-auth-check: LLM call failed', { url, error: String(err) });
|
|
124
|
+
// If LLM call fails, default to not authorized
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -7,7 +7,9 @@ exports.MAX_TOOL_CONTENT_CHARS = exports.MAX_WEB_FETCH_BYTES = exports.WEB_SEARC
|
|
|
7
7
|
exports.executeWebSearch = executeWebSearch;
|
|
8
8
|
exports.executeTool = executeTool;
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
10
|
-
const config_1 = require("
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const browser_playwright_1 = require("./browser-playwright");
|
|
12
|
+
const llm_auth_check_1 = require("./llm-auth-check");
|
|
11
13
|
exports.WEB_FETCH_TOOL = {
|
|
12
14
|
name: 'web_fetch',
|
|
13
15
|
description: "Fetch the text content of any publicly accessible URL. Use this to retrieve documentation, error references, API guides, release notes, or any web resource that would help answer the user's question.",
|
|
@@ -134,30 +136,113 @@ async function executeWebSearch(query, log) {
|
|
|
134
136
|
log.info('web_search: using DuckDuckGo (free fallback)', { query });
|
|
135
137
|
return formatSearchResults(await searchWithDuckDuckGo(query));
|
|
136
138
|
}
|
|
139
|
+
function stripHtml(raw) {
|
|
140
|
+
return raw
|
|
141
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
142
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
143
|
+
.replace(/<[^>]+>/g, ' ')
|
|
144
|
+
.replace(/\s+/g, ' ')
|
|
145
|
+
.trim();
|
|
146
|
+
}
|
|
147
|
+
const BASE_FETCH_HEADERS = {
|
|
148
|
+
'User-Agent': 'Mozilla/5.0 (compatible; OmniKeyAgent/1.0)',
|
|
149
|
+
};
|
|
150
|
+
// ── Step 1: plain HTTP fetch ──────────────────────────────────────────────────
|
|
151
|
+
async function fetchPlainHttp(url, log) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await axios_1.default.get(url, {
|
|
154
|
+
timeout: 15000,
|
|
155
|
+
responseType: 'text',
|
|
156
|
+
maxContentLength: exports.MAX_WEB_FETCH_BYTES,
|
|
157
|
+
headers: BASE_FETCH_HEADERS,
|
|
158
|
+
});
|
|
159
|
+
const finalUrl = response.request?.res?.responseUrl ?? url;
|
|
160
|
+
return { html: String(response.data), authBlocked: false, finalUrl };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const status = axios_1.default.isAxiosError(err) ? err.response?.status : undefined;
|
|
164
|
+
log.warn('Initial fetch failed', {
|
|
165
|
+
url,
|
|
166
|
+
error: err instanceof Error ? err.message : String(err),
|
|
167
|
+
status,
|
|
168
|
+
});
|
|
169
|
+
// If a browser is running, any failure could be auth-related —
|
|
170
|
+
// sites use redirects, 302s, custom error pages, or soft-blocks
|
|
171
|
+
// rather than a clean 401/403, so checking status codes alone is
|
|
172
|
+
// unreliable. Fall through to the browser-session path instead.
|
|
173
|
+
if (isSelfHostedMacOS && (0, browser_playwright_1.isBrowserOpenWithUrl)(url, log)) {
|
|
174
|
+
return { html: null, authBlocked: true, finalUrl: url };
|
|
175
|
+
}
|
|
176
|
+
if (status === 401 || status === 403) {
|
|
177
|
+
return { html: null, authBlocked: true, finalUrl: url };
|
|
178
|
+
}
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// ── Step 2: LLM auth check on plain response ──────────────────────────────────
|
|
183
|
+
async function checkPlainResponseAuth(plainText, url, log, finalUrl) {
|
|
184
|
+
const authenticated = await (0, llm_auth_check_1.isPageAuthenticated)(plainText.slice(0, 5000), url, log, finalUrl);
|
|
185
|
+
if (!authenticated) {
|
|
186
|
+
log.info('web_fetch: plain response failed auth check — trying active-tab strategy', { url });
|
|
187
|
+
}
|
|
188
|
+
return authenticated;
|
|
189
|
+
}
|
|
190
|
+
// ── Step 3: active-tab extraction (self-hosted macOS only) ───────────────────
|
|
191
|
+
async function fetchFromActiveTab(url, log) {
|
|
192
|
+
log.info('web_fetch: falling back to active-tab extraction', { url });
|
|
193
|
+
return (0, browser_playwright_1.fetchWithPlaywright)(url, log);
|
|
194
|
+
}
|
|
195
|
+
const isSelfHostedMacOS = config_1.config.isSelfHosted && config_1.config.terminalPlatform === 'macos';
|
|
196
|
+
async function executeWebFetch(url, log) {
|
|
197
|
+
log.info('Executing web_fetch tool', { url });
|
|
198
|
+
// ── Step 1: plain HTTP request ────────────────────────────────────────────
|
|
199
|
+
const { html, authBlocked, finalUrl } = await fetchPlainHttp(url, log);
|
|
200
|
+
const plainText = html ? stripHtml(html) : '';
|
|
201
|
+
if (!isSelfHostedMacOS) {
|
|
202
|
+
if (authBlocked) {
|
|
203
|
+
log.warn('Error: page requires authentication. Run OmniKey in self-hosted mode on macOS to enable browser-session access.');
|
|
204
|
+
}
|
|
205
|
+
return plainText.slice(0, exports.MAX_TOOL_CONTENT_CHARS) || 'No content retrieved';
|
|
206
|
+
}
|
|
207
|
+
// ── Step 2 (self-hosted macOS only): LLM auth check on plain response ─────
|
|
208
|
+
let looksUnauthenticated = false;
|
|
209
|
+
if (!authBlocked && plainText) {
|
|
210
|
+
log.info('web_fetch: performing LLM auth check on plain HTTP response', { url });
|
|
211
|
+
const authenticated = await checkPlainResponseAuth(plainText, url, log, finalUrl);
|
|
212
|
+
if (authenticated) {
|
|
213
|
+
return plainText.slice(0, exports.MAX_TOOL_CONTENT_CHARS) || 'No content retrieved';
|
|
214
|
+
}
|
|
215
|
+
looksUnauthenticated = true;
|
|
216
|
+
}
|
|
217
|
+
// ── Step 3 (self-hosted macOS only): active-tab extraction ───────────────
|
|
218
|
+
// Only attempted when there is evidence authentication is required.
|
|
219
|
+
const needsAuth = authBlocked || looksUnauthenticated;
|
|
220
|
+
if (needsAuth) {
|
|
221
|
+
log.info('web_fetch: evidence of authentication requirement, attempting active-tab extraction', { url });
|
|
222
|
+
const activeTabText = await fetchFromActiveTab(url, log);
|
|
223
|
+
if (activeTabText) {
|
|
224
|
+
return activeTabText.slice(0, exports.MAX_TOOL_CONTENT_CHARS);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// All strategies exhausted.
|
|
228
|
+
if (authBlocked) {
|
|
229
|
+
log.warn('Error: page requires authentication. Open the page in Chrome and ensure "Allow JavaScript from Apple Events" is enabled (View → Developer → Allow JavaScript from Apple Events).');
|
|
230
|
+
}
|
|
231
|
+
return plainText.slice(0, exports.MAX_TOOL_CONTENT_CHARS) || 'No content retrieved';
|
|
232
|
+
}
|
|
137
233
|
async function executeTool(name, args, log) {
|
|
138
234
|
if (name === 'web_fetch') {
|
|
139
235
|
const url = args.url;
|
|
140
236
|
if (!url)
|
|
141
237
|
return 'Error: url parameter is required';
|
|
142
238
|
try {
|
|
143
|
-
|
|
144
|
-
const response = await axios_1.default.get(url, {
|
|
145
|
-
timeout: 15000,
|
|
146
|
-
responseType: 'text',
|
|
147
|
-
maxContentLength: exports.MAX_WEB_FETCH_BYTES,
|
|
148
|
-
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; OmniKeyAgent/1.0)' },
|
|
149
|
-
});
|
|
150
|
-
const text = String(response.data)
|
|
151
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
152
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
153
|
-
.replace(/<[^>]+>/g, ' ')
|
|
154
|
-
.replace(/\s+/g, ' ')
|
|
155
|
-
.trim()
|
|
156
|
-
.slice(0, exports.MAX_TOOL_CONTENT_CHARS);
|
|
157
|
-
return text || 'No content retrieved';
|
|
239
|
+
return await executeWebFetch(url, log);
|
|
158
240
|
}
|
|
159
241
|
catch (err) {
|
|
160
|
-
log.warn('web_fetch tool failed', {
|
|
242
|
+
log.warn('web_fetch tool failed', {
|
|
243
|
+
url,
|
|
244
|
+
error: err instanceof Error ? err.message : String(err),
|
|
245
|
+
});
|
|
161
246
|
return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`;
|
|
162
247
|
}
|
|
163
248
|
}
|
|
@@ -170,7 +255,10 @@ async function executeTool(name, args, log) {
|
|
|
170
255
|
return await executeWebSearch(query, log);
|
|
171
256
|
}
|
|
172
257
|
catch (err) {
|
|
173
|
-
log.warn('web_search tool failed', {
|
|
258
|
+
log.warn('web_search tool failed', {
|
|
259
|
+
query,
|
|
260
|
+
error: err instanceof Error ? err.message : String(err),
|
|
261
|
+
});
|
|
174
262
|
return `Error searching: ${err instanceof Error ? err.message : String(err)}`;
|
|
175
263
|
}
|
|
176
264
|
}
|
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.25",
|
|
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",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"sqlite3": "^5.1.6",
|
|
45
45
|
"winston": "^3.19.0",
|
|
46
46
|
"ws": "^8.18.0",
|
|
47
|
-
"zod": "^4.3.6"
|
|
47
|
+
"zod": "^4.3.6",
|
|
48
|
+
"playwright-core": "^1.50.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/inquirer": "^9.0.9",
|
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
|
|