omnikey-cli 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -9
- package/backend-dist/agentPrompts.js +62 -2
- package/backend-dist/agentServer.js +4 -3
- package/backend-dist/index.js +38 -2
- package/dist/daemon.js +71 -39
- package/dist/index.js +2 -4
- package/dist/killDaemon.js +36 -15
- package/dist/onboard.js +3 -3
- package/dist/removeConfig.js +50 -21
- package/dist/showLogs.js +2 -3
- package/dist/status.js +9 -22
- package/dist/utils.js +54 -0
- package/package.json +1 -1
- package/src/daemon.ts +88 -38
- package/src/index.ts +2 -4
- package/src/killDaemon.ts +36 -16
- package/src/onboard.ts +3 -3
- package/src/removeConfig.ts +48 -21
- package/src/showLogs.ts +2 -3
- package/src/status.ts +10 -19
- package/src/utils.ts +47 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A command-line tool for onboarding users to the Omnikey open-source app and configuring their OPENAI_API_KEY.
|
|
4
4
|
|
|
5
|
-
## About OmnikeyAI
|
|
5
|
+
## About OmnikeyAI
|
|
6
6
|
|
|
7
|
-
OmnikeyAI is a productivity tool
|
|
7
|
+
OmnikeyAI is a productivity tool that helps you quickly rewrite selected text using OpenAI. The CLI allows you to configure and run the backend daemon on your local machine and manage your OpenAI API key with ease.
|
|
8
8
|
|
|
9
9
|
- For more details about the app and its features, see the [main README](https://github.com/GurinderRawala/OmniKey-AI).
|
|
10
10
|
- Download the latest macOS app here: [Download OmniKeyAI for macOS](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/macos/download)
|
|
@@ -13,7 +13,7 @@ OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite select
|
|
|
13
13
|
|
|
14
14
|
- `omnikey onboard`: Interactive onboarding to set up your OPENAI_API_KEY.
|
|
15
15
|
- Accepts the `--open-ai-key` parameter for non-interactive setup.
|
|
16
|
-
- Configure and run the backend daemon
|
|
16
|
+
- Configure and run the backend daemon — persisted across reboots on both macOS and Windows.
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
@@ -21,28 +21,42 @@ OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite select
|
|
|
21
21
|
# Install CLI globally (from this directory)
|
|
22
22
|
npm install -g omnikey-cli
|
|
23
23
|
|
|
24
|
-
# Onboard interactively (will prompt for OpenAI key
|
|
24
|
+
# Onboard interactively (will prompt for OpenAI key)
|
|
25
25
|
omnikey onboard
|
|
26
26
|
|
|
27
27
|
# Or onboard non-interactively
|
|
28
28
|
omnikey onboard --open-ai-key YOUR_KEY
|
|
29
29
|
|
|
30
|
-
#
|
|
30
|
+
# Start the daemon (auto-restarts on reboot)
|
|
31
31
|
omnikey daemon --port 7071
|
|
32
32
|
|
|
33
33
|
# Kill the daemon
|
|
34
|
-
omnikey kill-daemon
|
|
34
|
+
omnikey kill-daemon
|
|
35
35
|
|
|
36
|
-
# Remove the config directory and SQLite database (and
|
|
36
|
+
# Remove the config directory and SQLite database (and persistence agent)
|
|
37
37
|
omnikey remove-config
|
|
38
38
|
|
|
39
|
-
#
|
|
39
|
+
# Check daemon status
|
|
40
40
|
omnikey status
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# Check daemon logs
|
|
43
43
|
omnikey logs --lines 100
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Platform notes
|
|
47
|
+
|
|
48
|
+
### macOS
|
|
49
|
+
|
|
50
|
+
The daemon is registered as a **launchd agent** (`~/Library/LaunchAgents/com.omnikey.daemon.plist`) so it auto-restarts after login and on crashes.
|
|
51
|
+
|
|
52
|
+
### Windows
|
|
53
|
+
|
|
54
|
+
The daemon is registered as a **Windows Task Scheduler** task (`OmnikeyDaemon`) that runs at every logon. A wrapper script (`~/.omnikey/start-daemon.cmd`) is generated to set the required environment variables before launching the Node.js backend.
|
|
55
|
+
|
|
56
|
+
> **Note:** `schtasks` is a built-in Windows command — no third-party tools or administrator rights are required for user-level scheduled tasks.
|
|
57
|
+
|
|
58
|
+
Commands that query process state use `netstat` (instead of `lsof`) on Windows, and process termination uses `taskkill` (instead of `SIGTERM`).
|
|
59
|
+
|
|
46
60
|
## Development
|
|
47
61
|
|
|
48
62
|
- Built with Node.js and TypeScript.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
3
|
+
exports.AGENT_SYSTEM_PROMPT_WINDOWS = exports.AGENT_SYSTEM_PROMPT_MACOS = void 0;
|
|
4
|
+
exports.AGENT_SYSTEM_PROMPT_MACOS = `
|
|
5
5
|
You are an AI agent that can both reason about the user's situation and design shell scripts that the user will run on their own machine.
|
|
6
6
|
|
|
7
7
|
This agent is invoked when the user includes @omniAgent and there may also be stored custom task instructions for the current task.
|
|
@@ -62,3 +62,63 @@ The user will run the script and share the output with you.
|
|
|
62
62
|
- Do not emit any text before or after the <final_answer> block; the entire response must be inside the <final_answer> tags.
|
|
63
63
|
</final_answer_block>
|
|
64
64
|
`;
|
|
65
|
+
exports.AGENT_SYSTEM_PROMPT_WINDOWS = `
|
|
66
|
+
You are an AI agent that can both reason about the user's situation and design shell scripts that the user will run on their own machine.
|
|
67
|
+
|
|
68
|
+
This agent is invoked when the user includes @omniAgent and there may also be stored custom task instructions for the current task.
|
|
69
|
+
Your job is to:
|
|
70
|
+
- Read and respect the stored task instructions (how to behave, what to focus on, output style) when they are provided.
|
|
71
|
+
- Carefully consider the current user input (what they typed when running @omniAgent).
|
|
72
|
+
- Decide whether additional machine-level information is needed, and if so, generate an appropriate shell script to gather it.
|
|
73
|
+
- Use the results of any previously run scripts plus the instructions and input to produce a complete, helpful final answer.
|
|
74
|
+
|
|
75
|
+
General guidelines:
|
|
76
|
+
- Only create commands that are safe and read-only, focusing on inspection, diagnostics, and information gathering.
|
|
77
|
+
- Do not generate any commands that install software, modify user data, or change system settings.
|
|
78
|
+
- Never ask the user to run commands with elevated privileges (Run as Administrator).
|
|
79
|
+
- Ensure that all commands provided are compatible with Windows PowerShell; avoid any macOS or Linux-specific commands.
|
|
80
|
+
- Scripts must be self-contained and ready to run as-is, without the user needing to edit them.
|
|
81
|
+
|
|
82
|
+
The user will run the script and share the output with you.
|
|
83
|
+
|
|
84
|
+
<instruction_handling>
|
|
85
|
+
- Treat stored task instructions (if present) as authoritative for how to prioritize, what to examine, and how to format your answer, as long as they do not conflict with system rules or safety guidelines.
|
|
86
|
+
- Treat the current user input as the immediate goal or question you must solve, applying the stored instructions to that specific situation.
|
|
87
|
+
- If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
|
|
88
|
+
</instruction_handling>
|
|
89
|
+
|
|
90
|
+
<interaction_rules>
|
|
91
|
+
- When you need to execute ANY shell command, respond with a single <shell_script> block that contains the FULL script to run.
|
|
92
|
+
- Within that script, include all steps needed to carry out the current diagnostic or information-gathering task as completely as possible (for example, collect all relevant logs, inspect all relevant services, perform all necessary checks), rather than issuing minimal or placeholder commands.
|
|
93
|
+
- Prefer one comprehensive script over multiple small scripts; only wait for another round of output if you genuinely need the previous results to decide on the next actions.
|
|
94
|
+
- If further machine-level investigation is unnecessary, skip the shell script and respond directly with a <final_answer>.
|
|
95
|
+
- Every response MUST be exactly one of:
|
|
96
|
+
- A single <shell_script>...</shell_script> block, and nothing else; or
|
|
97
|
+
- A single <final_answer>...</final_answer> block, and nothing else.
|
|
98
|
+
- Never send plain text or explanation outside of these tags. If you are not emitting a <shell_script>, you MUST emit a <final_answer>.
|
|
99
|
+
- When you are completely finished and ready to present the result back to the user, respond with a single <final_answer> block.
|
|
100
|
+
- Do NOT include reasoning, commentary, or any other tags outside of <shell_script>...</shell_script> or <final_answer>...</final_answer>.
|
|
101
|
+
- Never wrap your entire response in other XML or JSON structures.
|
|
102
|
+
</interaction_rules>
|
|
103
|
+
|
|
104
|
+
<shell_script_block>
|
|
105
|
+
- Always emit exactly this structure when you want to run commands:
|
|
106
|
+
|
|
107
|
+
<shell_script>
|
|
108
|
+
# your commands here
|
|
109
|
+
</shell_script>
|
|
110
|
+
|
|
111
|
+
- Use a single, self-contained PowerShell script per turn; do not send multiple <shell_script> blocks in one response.
|
|
112
|
+
- Inside the script, group related commands logically and add brief inline comments ONLY when they clarify non-obvious steps.
|
|
113
|
+
- Prefer safe, idempotent commands. Never use elevated privileges.
|
|
114
|
+
- Use PowerShell cmdlets and syntax (e.g. Get-ChildItem, Select-Object, Where-Object) rather than cmd.exe or bash equivalents.
|
|
115
|
+
</shell_script_block>
|
|
116
|
+
|
|
117
|
+
<final_answer_block>
|
|
118
|
+
- When you have gathered enough information and completed the requested work, respond once with:
|
|
119
|
+
<final_answer>
|
|
120
|
+
...user-facing result here (clear summary, key findings, concrete recommendations or next steps, formatted according to any stored instructions)...
|
|
121
|
+
</final_answer>
|
|
122
|
+
- Do not emit any text before or after the <final_answer> block; the entire response must be inside the <final_answer> tags.
|
|
123
|
+
</final_answer_block>
|
|
124
|
+
`;
|
|
@@ -53,7 +53,7 @@ const openai = new openai_1.default({
|
|
|
53
53
|
});
|
|
54
54
|
const sessionMessages = new Map();
|
|
55
55
|
const MAX_TURNS = 10;
|
|
56
|
-
async function getOrCreateSession(sessionId, subscription, log) {
|
|
56
|
+
async function getOrCreateSession(sessionId, subscription, platform, log) {
|
|
57
57
|
const existing = sessionMessages.get(sessionId);
|
|
58
58
|
if (existing) {
|
|
59
59
|
log.debug('Reusing existing agent session', {
|
|
@@ -63,6 +63,7 @@ async function getOrCreateSession(sessionId, subscription, log) {
|
|
|
63
63
|
});
|
|
64
64
|
return existing;
|
|
65
65
|
}
|
|
66
|
+
const systemPrompt = platform === 'windows' ? agentPrompts_1.AGENT_SYSTEM_PROMPT_WINDOWS : agentPrompts_1.AGENT_SYSTEM_PROMPT_MACOS;
|
|
66
67
|
// use these instructions as user instructions
|
|
67
68
|
const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
|
|
68
69
|
log.error('Failed to get system prompt for new agent session', { error: err });
|
|
@@ -73,7 +74,7 @@ async function getOrCreateSession(sessionId, subscription, log) {
|
|
|
73
74
|
history: [
|
|
74
75
|
{
|
|
75
76
|
role: 'system',
|
|
76
|
-
content:
|
|
77
|
+
content: systemPrompt,
|
|
77
78
|
},
|
|
78
79
|
...(prompt
|
|
79
80
|
? [
|
|
@@ -163,7 +164,7 @@ async function authenticateFromAuthHeader(authHeader, log) {
|
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
166
|
-
const session = await getOrCreateSession(sessionId, subscription, log);
|
|
167
|
+
const session = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
|
|
167
168
|
// Count this call as one agent iteration.
|
|
168
169
|
session.turns += 1;
|
|
169
170
|
log.info('Starting agent turn', {
|
package/backend-dist/index.js
CHANGED
|
@@ -51,8 +51,8 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
51
51
|
const appcastUrl = `${baseUrl}/macos/appcast`;
|
|
52
52
|
// These should match the values embedded into the macOS app
|
|
53
53
|
// Info.plist in macOS/build_release_dmg.sh.
|
|
54
|
-
const bundleVersion = '
|
|
55
|
-
const shortVersion = '1.0.
|
|
54
|
+
const bundleVersion = '13';
|
|
55
|
+
const shortVersion = '1.0.12';
|
|
56
56
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
|
57
57
|
<rss version="2.0"
|
|
58
58
|
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
|
@@ -77,6 +77,42 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
77
77
|
res.set('Content-Type', 'application/xml; charset=utf-8');
|
|
78
78
|
res.send(xml);
|
|
79
79
|
});
|
|
80
|
+
// ── Windows distribution endpoints ───────────────────────────────────────────
|
|
81
|
+
// These should match the values in windows/OmniKey.Windows.csproj
|
|
82
|
+
// <Version> and windows/build_release_zip.ps1 $APP_VERSION.
|
|
83
|
+
const WIN_VERSION = '1.0';
|
|
84
|
+
const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-x64.zip';
|
|
85
|
+
const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
|
|
86
|
+
// Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
|
|
87
|
+
app.get('/windows/download', (_req, res) => {
|
|
88
|
+
res.download(WIN_ZIP_PATH, WIN_ZIP_FILENAME, (err) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
logger_1.logger.error('Failed to send Windows ZIP for download.', { error: err });
|
|
91
|
+
if (!res.headersSent) {
|
|
92
|
+
res.status(500).send('Unable to download file.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
// JSON update-check endpoint consumed by UpdateChecker.cs on the Windows client.
|
|
98
|
+
// Returns the latest version + download URL so the client can decide whether
|
|
99
|
+
// to prompt the user for an update.
|
|
100
|
+
app.get('/windows/update', (req, res) => {
|
|
101
|
+
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
|
102
|
+
let fileSize = 0;
|
|
103
|
+
try {
|
|
104
|
+
fileSize = fs_1.default.statSync(WIN_ZIP_PATH).size;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger_1.logger.error('Failed to stat Windows ZIP for update endpoint.', { error });
|
|
108
|
+
}
|
|
109
|
+
res.json({
|
|
110
|
+
version: WIN_VERSION,
|
|
111
|
+
downloadUrl: `${baseUrl}/windows/download`,
|
|
112
|
+
fileSize,
|
|
113
|
+
releaseNotes: '',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
80
116
|
app.get('/health', (_req, res) => {
|
|
81
117
|
res.json({ status: 'ok' });
|
|
82
118
|
});
|
package/dist/daemon.js
CHANGED
|
@@ -7,33 +7,20 @@ exports.startDaemon = startDaemon;
|
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const os_1 = __importDefault(require("os"));
|
|
11
10
|
const child_process_2 = require("child_process");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
12
12
|
/**
|
|
13
13
|
* Start the Omnikey API backend as a daemon on the specified port.
|
|
14
|
-
*
|
|
14
|
+
* On macOS: creates and registers a launchd agent for persistence.
|
|
15
|
+
* On Windows: creates a wrapper script and registers a Windows Task Scheduler task.
|
|
15
16
|
* @param port The port to run the backend on
|
|
16
17
|
*/
|
|
17
18
|
function startDaemon(port = 7071) {
|
|
18
|
-
// Only use ~/.omnikey/config.json for environment variables
|
|
19
|
-
// Path to the backend entry point (now from backend-dist)
|
|
20
19
|
const backendPath = path_1.default.resolve(__dirname, '../backend-dist/index.js');
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const configPath = path_1.default.join(configDir, 'config.json');
|
|
25
|
-
let configVars = {};
|
|
26
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
27
|
-
try {
|
|
28
|
-
configVars = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
29
|
-
}
|
|
30
|
-
catch (e) {
|
|
31
|
-
console.error('Failed to parse config.json:', e);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Ensure both OMNIKEY_PORT and PORT are set for compatibility
|
|
20
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
21
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
22
|
+
const configVars = (0, utils_1.readConfig)();
|
|
35
23
|
configVars.OMNIKEY_PORT = port;
|
|
36
|
-
// Write the updated configVars back to config.json
|
|
37
24
|
try {
|
|
38
25
|
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
39
26
|
fs_1.default.writeFileSync(configPath, JSON.stringify(configVars, null, 2), 'utf-8');
|
|
@@ -41,10 +28,69 @@ function startDaemon(port = 7071) {
|
|
|
41
28
|
catch (e) {
|
|
42
29
|
console.error('Failed to write updated config.json:', e);
|
|
43
30
|
}
|
|
44
|
-
|
|
31
|
+
const nodePath = process.execPath;
|
|
32
|
+
const logPath = path_1.default.join(configDir, 'daemon.log');
|
|
33
|
+
const errorLogPath = path_1.default.join(configDir, 'daemon-error.log');
|
|
34
|
+
if (utils_1.isWindows) {
|
|
35
|
+
startDaemonWindows({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function startDaemonWindows(opts) {
|
|
42
|
+
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
43
|
+
// Write a wrapper .cmd script that sets env vars and launches the backend
|
|
44
|
+
const wrapperPath = path_1.default.join(configDir, 'start-daemon.cmd');
|
|
45
|
+
const envSetLines = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
|
|
46
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
47
|
+
.join('\r\n');
|
|
48
|
+
const wrapperContent = [
|
|
49
|
+
'@echo off',
|
|
50
|
+
envSetLines,
|
|
51
|
+
`"${nodePath}" "${backendPath}" >> "${logPath}" 2>> "${errorLogPath}"`,
|
|
52
|
+
'',
|
|
53
|
+
].join('\r\n');
|
|
54
|
+
try {
|
|
55
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
56
|
+
fs_1.default.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
console.error('Failed to write start-daemon.cmd:', e);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Register with Windows Task Scheduler so the daemon persists across reboots
|
|
63
|
+
const taskName = 'OmnikeyDaemon';
|
|
64
|
+
try {
|
|
65
|
+
// Delete existing task silently before creating a fresh one
|
|
66
|
+
(0, child_process_2.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Task may not exist — that's fine
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
(0, child_process_2.execSync)(`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`, { stdio: 'pipe' });
|
|
73
|
+
console.log(`Windows Task Scheduler task created: ${taskName}`);
|
|
74
|
+
console.log('Omnikey daemon will auto-start on next logon.');
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.error('Failed to create Windows Task Scheduler task:', e);
|
|
78
|
+
}
|
|
79
|
+
// Also start the backend immediately for the current session
|
|
80
|
+
const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
81
|
+
const child = (0, child_process_1.spawn)(nodePath, [backendPath], {
|
|
82
|
+
env: { ...process.env, ...configVars, OMNIKEY_PORT: String(port) },
|
|
83
|
+
detached: true,
|
|
84
|
+
stdio: ['ignore', out, err],
|
|
85
|
+
});
|
|
86
|
+
child.unref();
|
|
87
|
+
console.log(`Omnikey API backend started as a daemon on port ${port}. PID: ${child.pid}`);
|
|
88
|
+
}
|
|
89
|
+
function startDaemonMacOS(opts) {
|
|
90
|
+
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
91
|
+
const homeDir = (0, utils_1.getHomeDir)();
|
|
45
92
|
const plistName = 'com.omnikey.daemon.plist';
|
|
46
93
|
const plistPath = path_1.default.join(homeDir, 'Library', 'LaunchAgents', plistName);
|
|
47
|
-
const nodePath = process.execPath;
|
|
48
94
|
const envVars = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
|
|
49
95
|
.map(([k, v]) => `<key>${k}</key><string>${v}</string>`)
|
|
50
96
|
.join('\n');
|
|
@@ -68,21 +114,19 @@ function startDaemon(port = 7071) {
|
|
|
68
114
|
<key>KeepAlive</key>
|
|
69
115
|
<true/>
|
|
70
116
|
<key>StandardOutPath</key>
|
|
71
|
-
<string>${
|
|
117
|
+
<string>${logPath}</string>
|
|
72
118
|
<key>StandardErrorPath</key>
|
|
73
|
-
<string>${
|
|
119
|
+
<string>${errorLogPath}</string>
|
|
74
120
|
<key>WorkingDirectory</key>
|
|
75
121
|
<string>${configDir}</string>
|
|
76
122
|
</dict>
|
|
77
123
|
</plist>
|
|
78
124
|
`;
|
|
79
|
-
// Write plist file
|
|
80
125
|
try {
|
|
81
126
|
const launchAgentsDir = path_1.default.join(homeDir, 'Library', 'LaunchAgents');
|
|
82
127
|
fs_1.default.mkdirSync(launchAgentsDir, { recursive: true });
|
|
83
128
|
fs_1.default.writeFileSync(plistPath, plistContent, 'utf-8');
|
|
84
|
-
|
|
85
|
-
(0, child_process_2.execSync)(`launchctl unload "${plistPath}" || true`); // Unload if already loaded
|
|
129
|
+
(0, child_process_2.execSync)(`launchctl unload "${plistPath}" || true`);
|
|
86
130
|
(0, child_process_2.execSync)(`launchctl load "${plistPath}"`);
|
|
87
131
|
console.log(`Launch agent created and loaded: ${plistPath}`);
|
|
88
132
|
console.log('Omnikey daemon will auto-restart and persist across reboots.');
|
|
@@ -90,19 +134,7 @@ function startDaemon(port = 7071) {
|
|
|
90
134
|
catch (e) {
|
|
91
135
|
console.error('Failed to create or load launch agent:', e);
|
|
92
136
|
}
|
|
93
|
-
|
|
94
|
-
const logPath = path_1.default.join(configDir, 'daemon.log');
|
|
95
|
-
const errorLogPath = path_1.default.join(configDir, 'daemon-error.log');
|
|
96
|
-
// Clean (truncate) log files before starting new session
|
|
97
|
-
try {
|
|
98
|
-
fs_1.default.writeFileSync(logPath, '');
|
|
99
|
-
fs_1.default.writeFileSync(errorLogPath, '');
|
|
100
|
-
}
|
|
101
|
-
catch (e) {
|
|
102
|
-
// Ignore errors if files don't exist yet
|
|
103
|
-
}
|
|
104
|
-
const out = fs_1.default.openSync(logPath, 'a');
|
|
105
|
-
const err = fs_1.default.openSync(errorLogPath, 'a');
|
|
137
|
+
const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
106
138
|
const child = (0, child_process_1.spawn)('node', [backendPath], {
|
|
107
139
|
env: { ...configVars, OMNIKEY_PORT: String(port) },
|
|
108
140
|
detached: true,
|
package/dist/index.js
CHANGED
|
@@ -31,10 +31,8 @@ program
|
|
|
31
31
|
program
|
|
32
32
|
.command('kill-daemon')
|
|
33
33
|
.description('Kill the Omnikey API backend daemon running on a specified port')
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
const port = Number(options.port) || 7071;
|
|
37
|
-
(0, killDaemon_1.killDaemon)(port);
|
|
34
|
+
.action(() => {
|
|
35
|
+
(0, killDaemon_1.killDaemon)();
|
|
38
36
|
});
|
|
39
37
|
program
|
|
40
38
|
.command('remove-config')
|
package/dist/killDaemon.js
CHANGED
|
@@ -3,37 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.killDaemon = killDaemon;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const removeConfig_1 = require("./removeConfig");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
6
7
|
/**
|
|
7
|
-
* Kill the Omnikey API backend daemon
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Kill the Omnikey API backend daemon.
|
|
9
|
+
* Reads the port from ~/.omnikey/config.json (falls back to 7071).
|
|
10
|
+
* Removes the persistence agent, then kills any remaining process on the port.
|
|
10
11
|
*/
|
|
11
|
-
function killDaemon(
|
|
12
|
-
|
|
12
|
+
function killDaemon() {
|
|
13
|
+
const port = (0, utils_1.getPort)();
|
|
14
|
+
// 1. Remove/stop the persistence agent
|
|
13
15
|
try {
|
|
14
|
-
(0, removeConfig_1.
|
|
15
|
-
console.log('
|
|
16
|
+
(0, removeConfig_1.killPersistenceAgent)();
|
|
17
|
+
console.log('Persistence agent stopped (if it existed).');
|
|
16
18
|
}
|
|
17
19
|
catch (e) {
|
|
18
|
-
console.warn('Failed to
|
|
20
|
+
console.warn('Failed to stop persistence agent:', e);
|
|
19
21
|
}
|
|
20
|
-
// 2.
|
|
22
|
+
// 2. Find any remaining processes still using the port
|
|
21
23
|
let pids = [];
|
|
22
24
|
try {
|
|
23
|
-
|
|
25
|
+
if (utils_1.isWindows) {
|
|
26
|
+
// netstat -ano lists PID in the last column; filter by :<port> with LISTENING or ESTABLISHED
|
|
27
|
+
const output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port}`).toString();
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const line of output.trim().split('\n')) {
|
|
30
|
+
const parts = line.trim().split(/\s+/);
|
|
31
|
+
const pid = parts[parts.length - 1];
|
|
32
|
+
if (pid && /^\d+$/.test(pid) && pid !== '0' && !seen.has(pid)) {
|
|
33
|
+
seen.add(pid);
|
|
34
|
+
pids.push(pid);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
pids = (0, child_process_1.execSync)(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
|
|
40
|
+
}
|
|
24
41
|
}
|
|
25
|
-
catch
|
|
26
|
-
// lsof returns non-zero exit code if nothing is using the port
|
|
42
|
+
catch {
|
|
27
43
|
pids = [];
|
|
28
44
|
}
|
|
29
45
|
if (pids.length === 0) {
|
|
30
|
-
console.log(`No process found using port ${port}
|
|
46
|
+
console.log(`No process found using port ${port}.`);
|
|
31
47
|
return;
|
|
32
48
|
}
|
|
33
|
-
// 3.
|
|
49
|
+
// 3. Kill each process
|
|
34
50
|
for (const pid of pids) {
|
|
35
51
|
try {
|
|
36
|
-
|
|
52
|
+
if (utils_1.isWindows) {
|
|
53
|
+
(0, child_process_1.execSync)(`taskkill /PID ${pid} /F`, { stdio: 'pipe' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
57
|
+
}
|
|
37
58
|
console.log(`Killed process with PID ${pid} using port ${port}.`);
|
|
38
59
|
}
|
|
39
60
|
catch (e) {
|
package/dist/onboard.js
CHANGED
|
@@ -7,14 +7,14 @@ exports.onboard = onboard;
|
|
|
7
7
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const utils_1 = require("./utils");
|
|
10
11
|
/**
|
|
11
12
|
* Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
|
|
12
13
|
* @param openAiKey Optional key provided via CLI
|
|
13
14
|
*/
|
|
14
15
|
async function onboard(openAiKey) {
|
|
15
16
|
let apiKey = openAiKey;
|
|
16
|
-
const
|
|
17
|
-
const configDir = path_1.default.join(homeDir, '.omnikey');
|
|
17
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
18
18
|
const sqlitePath = path_1.default.join(configDir, 'omnikey-selfhosted.sqlite');
|
|
19
19
|
if (!apiKey) {
|
|
20
20
|
const answers = await inquirer_1.default.prompt([
|
|
@@ -28,7 +28,7 @@ async function onboard(openAiKey) {
|
|
|
28
28
|
apiKey = answers.apiKey;
|
|
29
29
|
}
|
|
30
30
|
// Save all environment variables to ~/.omnikey/config.json
|
|
31
|
-
const configPath =
|
|
31
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
32
32
|
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
33
33
|
const configVars = {
|
|
34
34
|
OPENAI_API_KEY: apiKey,
|
package/dist/removeConfig.js
CHANGED
|
@@ -4,15 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.killLaunchdAgent = killLaunchdAgent;
|
|
7
|
+
exports.killWindowsTask = killWindowsTask;
|
|
8
|
+
exports.killPersistenceAgent = killPersistenceAgent;
|
|
7
9
|
exports.removeConfigAndDb = removeConfigAndDb;
|
|
8
10
|
const fs_1 = __importDefault(require("fs"));
|
|
9
11
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const os_1 = __importDefault(require("os"));
|
|
11
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const utils_1 = require("./utils");
|
|
12
14
|
function killLaunchdAgent() {
|
|
13
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
|
|
14
15
|
const plistName = 'com.omnikey.daemon.plist';
|
|
15
|
-
const plistPath = path_1.default.join(
|
|
16
|
+
const plistPath = path_1.default.join((0, utils_1.getHomeDir)(), 'Library', 'LaunchAgents', plistName);
|
|
16
17
|
if (fs_1.default.existsSync(plistPath)) {
|
|
17
18
|
try {
|
|
18
19
|
(0, child_process_1.execSync)(`launchctl unload "${plistPath}"`);
|
|
@@ -27,30 +28,58 @@ function killLaunchdAgent() {
|
|
|
27
28
|
console.log(`Launchd agent does not exist: ${plistPath}`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
function killWindowsTask() {
|
|
32
|
+
const taskName = 'OmnikeyDaemon';
|
|
33
|
+
try {
|
|
34
|
+
(0, child_process_1.execSync)(`schtasks /end /tn "${taskName}"`, { stdio: 'pipe' });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Task may not be running — that's fine
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
(0, child_process_1.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
41
|
+
console.log(`Removed Windows Task Scheduler task: ${taskName}`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.log(`Windows Task Scheduler task does not exist: ${taskName}`);
|
|
45
|
+
}
|
|
46
|
+
// Also remove the wrapper script
|
|
47
|
+
const wrapperPath = path_1.default.join((0, utils_1.getConfigDir)(), 'start-daemon.cmd');
|
|
48
|
+
if (fs_1.default.existsSync(wrapperPath)) {
|
|
49
|
+
try {
|
|
50
|
+
fs_1.default.rmSync(wrapperPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Kill the platform-appropriate persistence agent (launchd on macOS, Task Scheduler on Windows).
|
|
59
|
+
*/
|
|
60
|
+
function killPersistenceAgent() {
|
|
61
|
+
if (utils_1.isWindows) {
|
|
62
|
+
killWindowsTask();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
killLaunchdAgent();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
30
68
|
/**
|
|
31
69
|
* Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
|
|
32
70
|
*/
|
|
33
71
|
function removeConfigAndDb() {
|
|
34
|
-
const homeDir =
|
|
35
|
-
const configDir =
|
|
36
|
-
const
|
|
72
|
+
const homeDir = (0, utils_1.getHomeDir)();
|
|
73
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
74
|
+
const configData = (0, utils_1.readConfig)();
|
|
37
75
|
let sqlitePath = path_1.default.join(homeDir, 'omnikey-selfhosted.sqlite');
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (configData.SQLITE_PATH) {
|
|
43
|
-
sqlitePath = path_1.default.isAbsolute(configData.SQLITE_PATH)
|
|
44
|
-
? configData.SQLITE_PATH
|
|
45
|
-
: path_1.default.join(homeDir, configData.SQLITE_PATH);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
console.error(`Failed to read config.json: ${e}`);
|
|
50
|
-
}
|
|
76
|
+
if (configData.SQLITE_PATH) {
|
|
77
|
+
sqlitePath = path_1.default.isAbsolute(configData.SQLITE_PATH)
|
|
78
|
+
? configData.SQLITE_PATH
|
|
79
|
+
: path_1.default.join(homeDir, configData.SQLITE_PATH);
|
|
51
80
|
}
|
|
52
|
-
// Remove
|
|
53
|
-
|
|
81
|
+
// Remove platform-appropriate persistence agent
|
|
82
|
+
killPersistenceAgent();
|
|
54
83
|
// Remove SQLite database
|
|
55
84
|
if (fs_1.default.existsSync(sqlitePath)) {
|
|
56
85
|
try {
|
package/dist/showLogs.js
CHANGED
|
@@ -6,15 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.showLogs = showLogs;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const utils_1 = require("./utils");
|
|
9
10
|
/**
|
|
10
11
|
* Show the logs of the running Omnikey daemon by printing the contents of the daemon log file.
|
|
11
12
|
* Prints the last N lines (default 50) for convenience.
|
|
12
13
|
* If errorsOnly is true, shows daemon-error.log instead.
|
|
13
14
|
*/
|
|
14
15
|
function showLogs(lines = 50, errorsOnly = false) {
|
|
15
|
-
const
|
|
16
|
-
const configDir = path_1.default.join(homeDir, '.omnikey');
|
|
17
|
-
const logPath = path_1.default.join(configDir, errorsOnly ? 'daemon-error.log' : 'daemon.log');
|
|
16
|
+
const logPath = path_1.default.join((0, utils_1.getConfigDir)(), errorsOnly ? 'daemon-error.log' : 'daemon.log');
|
|
18
17
|
if (!fs_1.default.existsSync(logPath)) {
|
|
19
18
|
console.log(errorsOnly ? 'No error logs found.' : 'No daemon logs found.');
|
|
20
19
|
return;
|
package/dist/status.js
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.statusCmd = statusCmd;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
10
6
|
function statusCmd() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
17
|
-
try {
|
|
18
|
-
const configVars = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
19
|
-
if (configVars.OMNIKEY_PORT) {
|
|
20
|
-
port = Number(configVars.OMNIKEY_PORT);
|
|
21
|
-
}
|
|
7
|
+
const port = (0, utils_1.getPort)();
|
|
8
|
+
try {
|
|
9
|
+
let output;
|
|
10
|
+
if (utils_1.isWindows) {
|
|
11
|
+
output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port}`).toString();
|
|
22
12
|
}
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
else {
|
|
14
|
+
output = (0, child_process_1.execSync)(`lsof -i :${port}`).toString();
|
|
25
15
|
}
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const output = (0, child_process_1.execSync)(`lsof -i :${port}`).toString();
|
|
29
16
|
if (output.trim()) {
|
|
30
17
|
console.log(`Processes using port ${port}:\n${output}`);
|
|
31
18
|
}
|
|
@@ -33,7 +20,7 @@ function statusCmd() {
|
|
|
33
20
|
console.log(`No process is using port ${port}.`);
|
|
34
21
|
}
|
|
35
22
|
}
|
|
36
|
-
catch
|
|
23
|
+
catch {
|
|
37
24
|
console.log(`No process is using port ${port}.`);
|
|
38
25
|
}
|
|
39
26
|
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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.isWindows = void 0;
|
|
7
|
+
exports.getHomeDir = getHomeDir;
|
|
8
|
+
exports.getConfigDir = getConfigDir;
|
|
9
|
+
exports.getConfigPath = getConfigPath;
|
|
10
|
+
exports.readConfig = readConfig;
|
|
11
|
+
exports.getPort = getPort;
|
|
12
|
+
exports.initLogFiles = initLogFiles;
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
exports.isWindows = process.platform === 'win32';
|
|
17
|
+
function getHomeDir() {
|
|
18
|
+
return process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
|
|
19
|
+
}
|
|
20
|
+
function getConfigDir() {
|
|
21
|
+
return path_1.default.join(getHomeDir(), '.omnikey');
|
|
22
|
+
}
|
|
23
|
+
function getConfigPath() {
|
|
24
|
+
return path_1.default.join(getConfigDir(), 'config.json');
|
|
25
|
+
}
|
|
26
|
+
function readConfig() {
|
|
27
|
+
const configPath = getConfigPath();
|
|
28
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// fall through to empty config
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
function getPort() {
|
|
39
|
+
const config = readConfig();
|
|
40
|
+
return config.OMNIKEY_PORT ? Number(config.OMNIKEY_PORT) : 7071;
|
|
41
|
+
}
|
|
42
|
+
function initLogFiles(logPath, errorLogPath) {
|
|
43
|
+
try {
|
|
44
|
+
fs_1.default.writeFileSync(logPath, '');
|
|
45
|
+
fs_1.default.writeFileSync(errorLogPath, '');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore if files don't exist yet
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
out: fs_1.default.openSync(logPath, 'a'),
|
|
52
|
+
err: fs_1.default.openSync(errorLogPath, 'a'),
|
|
53
|
+
};
|
|
54
|
+
}
|
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.12",
|
|
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
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import { execSync } from 'child_process';
|
|
5
|
+
import { isWindows, getHomeDir, getConfigDir, getConfigPath, readConfig, initLogFiles } from './utils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Start the Omnikey API backend as a daemon on the specified port.
|
|
9
|
-
*
|
|
9
|
+
* On macOS: creates and registers a launchd agent for persistence.
|
|
10
|
+
* On Windows: creates a wrapper script and registers a Windows Task Scheduler task.
|
|
10
11
|
* @param port The port to run the backend on
|
|
11
12
|
*/
|
|
12
13
|
export function startDaemon(port: number = 7071) {
|
|
13
|
-
// Only use ~/.omnikey/config.json for environment variables
|
|
14
|
-
|
|
15
|
-
// Path to the backend entry point (now from backend-dist)
|
|
16
14
|
const backendPath = path.resolve(__dirname, '../backend-dist/index.js');
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const configPath = path.join(configDir, 'config.json');
|
|
22
|
-
let configVars: Record<string, any> = {};
|
|
23
|
-
if (fs.existsSync(configPath)) {
|
|
24
|
-
try {
|
|
25
|
-
configVars = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
26
|
-
} catch (e) {
|
|
27
|
-
console.error('Failed to parse config.json:', e);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Ensure both OMNIKEY_PORT and PORT are set for compatibility
|
|
16
|
+
const configDir = getConfigDir();
|
|
17
|
+
const configPath = getConfigPath();
|
|
18
|
+
const configVars = readConfig();
|
|
31
19
|
configVars.OMNIKEY_PORT = port;
|
|
32
|
-
// Write the updated configVars back to config.json
|
|
33
20
|
try {
|
|
34
21
|
fs.mkdirSync(configDir, { recursive: true });
|
|
35
22
|
fs.writeFileSync(configPath, JSON.stringify(configVars, null, 2), 'utf-8');
|
|
@@ -37,10 +24,86 @@ export function startDaemon(port: number = 7071) {
|
|
|
37
24
|
console.error('Failed to write updated config.json:', e);
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
const nodePath = process.execPath;
|
|
28
|
+
const logPath = path.join(configDir, 'daemon.log');
|
|
29
|
+
const errorLogPath = path.join(configDir, 'daemon-error.log');
|
|
30
|
+
|
|
31
|
+
if (isWindows) {
|
|
32
|
+
startDaemonWindows({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
|
|
33
|
+
} else {
|
|
34
|
+
startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DaemonOptions {
|
|
39
|
+
port: number;
|
|
40
|
+
configDir: string;
|
|
41
|
+
configVars: Record<string, any>;
|
|
42
|
+
nodePath: string;
|
|
43
|
+
backendPath: string;
|
|
44
|
+
logPath: string;
|
|
45
|
+
errorLogPath: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function startDaemonWindows(opts: DaemonOptions) {
|
|
49
|
+
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
50
|
+
|
|
51
|
+
// Write a wrapper .cmd script that sets env vars and launches the backend
|
|
52
|
+
const wrapperPath = path.join(configDir, 'start-daemon.cmd');
|
|
53
|
+
const envSetLines = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
|
|
54
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
55
|
+
.join('\r\n');
|
|
56
|
+
const wrapperContent = [
|
|
57
|
+
'@echo off',
|
|
58
|
+
envSetLines,
|
|
59
|
+
`"${nodePath}" "${backendPath}" >> "${logPath}" 2>> "${errorLogPath}"`,
|
|
60
|
+
'',
|
|
61
|
+
].join('\r\n');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('Failed to write start-daemon.cmd:', e);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Register with Windows Task Scheduler so the daemon persists across reboots
|
|
72
|
+
const taskName = 'OmnikeyDaemon';
|
|
73
|
+
try {
|
|
74
|
+
// Delete existing task silently before creating a fresh one
|
|
75
|
+
execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
76
|
+
} catch {
|
|
77
|
+
// Task may not exist — that's fine
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
execSync(
|
|
81
|
+
`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`,
|
|
82
|
+
{ stdio: 'pipe' },
|
|
83
|
+
);
|
|
84
|
+
console.log(`Windows Task Scheduler task created: ${taskName}`);
|
|
85
|
+
console.log('Omnikey daemon will auto-start on next logon.');
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('Failed to create Windows Task Scheduler task:', e);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Also start the backend immediately for the current session
|
|
91
|
+
const { out, err } = initLogFiles(logPath, errorLogPath);
|
|
92
|
+
const child = spawn(nodePath, [backendPath], {
|
|
93
|
+
env: { ...process.env, ...configVars, OMNIKEY_PORT: String(port) },
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', out, err],
|
|
96
|
+
});
|
|
97
|
+
child.unref();
|
|
98
|
+
console.log(`Omnikey API backend started as a daemon on port ${port}. PID: ${child.pid}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function startDaemonMacOS(opts: DaemonOptions) {
|
|
102
|
+
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
103
|
+
const homeDir = getHomeDir();
|
|
104
|
+
|
|
41
105
|
const plistName = 'com.omnikey.daemon.plist';
|
|
42
106
|
const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', plistName);
|
|
43
|
-
const nodePath = process.execPath;
|
|
44
107
|
const envVars = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
|
|
45
108
|
.map(([k, v]) => `<key>${k}</key><string>${v}</string>`)
|
|
46
109
|
.join('\n');
|
|
@@ -64,21 +127,19 @@ export function startDaemon(port: number = 7071) {
|
|
|
64
127
|
<key>KeepAlive</key>
|
|
65
128
|
<true/>
|
|
66
129
|
<key>StandardOutPath</key>
|
|
67
|
-
<string>${
|
|
130
|
+
<string>${logPath}</string>
|
|
68
131
|
<key>StandardErrorPath</key>
|
|
69
|
-
<string>${
|
|
132
|
+
<string>${errorLogPath}</string>
|
|
70
133
|
<key>WorkingDirectory</key>
|
|
71
134
|
<string>${configDir}</string>
|
|
72
135
|
</dict>
|
|
73
136
|
</plist>
|
|
74
137
|
`;
|
|
75
|
-
// Write plist file
|
|
76
138
|
try {
|
|
77
139
|
const launchAgentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
78
140
|
fs.mkdirSync(launchAgentsDir, { recursive: true });
|
|
79
141
|
fs.writeFileSync(plistPath, plistContent, 'utf-8');
|
|
80
|
-
|
|
81
|
-
execSync(`launchctl unload "${plistPath}" || true`); // Unload if already loaded
|
|
142
|
+
execSync(`launchctl unload "${plistPath}" || true`);
|
|
82
143
|
execSync(`launchctl load "${plistPath}"`);
|
|
83
144
|
console.log(`Launch agent created and loaded: ${plistPath}`);
|
|
84
145
|
console.log('Omnikey daemon will auto-restart and persist across reboots.');
|
|
@@ -86,18 +147,7 @@ export function startDaemon(port: number = 7071) {
|
|
|
86
147
|
console.error('Failed to create or load launch agent:', e);
|
|
87
148
|
}
|
|
88
149
|
|
|
89
|
-
|
|
90
|
-
const logPath = path.join(configDir, 'daemon.log');
|
|
91
|
-
const errorLogPath = path.join(configDir, 'daemon-error.log');
|
|
92
|
-
// Clean (truncate) log files before starting new session
|
|
93
|
-
try {
|
|
94
|
-
fs.writeFileSync(logPath, '');
|
|
95
|
-
fs.writeFileSync(errorLogPath, '');
|
|
96
|
-
} catch (e) {
|
|
97
|
-
// Ignore errors if files don't exist yet
|
|
98
|
-
}
|
|
99
|
-
const out = fs.openSync(logPath, 'a');
|
|
100
|
-
const err = fs.openSync(errorLogPath, 'a');
|
|
150
|
+
const { out, err } = initLogFiles(logPath, errorLogPath);
|
|
101
151
|
const child = spawn('node', [backendPath], {
|
|
102
152
|
env: { ...configVars, OMNIKEY_PORT: String(port) },
|
|
103
153
|
detached: true,
|
package/src/index.ts
CHANGED
|
@@ -35,10 +35,8 @@ program
|
|
|
35
35
|
program
|
|
36
36
|
.command('kill-daemon')
|
|
37
37
|
.description('Kill the Omnikey API backend daemon running on a specified port')
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
const port = Number(options.port) || 7071;
|
|
41
|
-
killDaemon(port);
|
|
38
|
+
.action(() => {
|
|
39
|
+
killDaemon();
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
program
|
package/src/killDaemon.ts
CHANGED
|
@@ -1,38 +1,58 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { killPersistenceAgent } from './removeConfig';
|
|
3
|
+
import { isWindows, getPort } from './utils';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Kill the Omnikey API backend daemon
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Kill the Omnikey API backend daemon.
|
|
7
|
+
* Reads the port from ~/.omnikey/config.json (falls back to 7071).
|
|
8
|
+
* Removes the persistence agent, then kills any remaining process on the port.
|
|
8
9
|
*/
|
|
9
|
-
export function killDaemon(
|
|
10
|
-
|
|
10
|
+
export function killDaemon() {
|
|
11
|
+
const port = getPort();
|
|
12
|
+
|
|
13
|
+
// 1. Remove/stop the persistence agent
|
|
11
14
|
try {
|
|
12
|
-
|
|
13
|
-
console.log('
|
|
15
|
+
killPersistenceAgent();
|
|
16
|
+
console.log('Persistence agent stopped (if it existed).');
|
|
14
17
|
} catch (e) {
|
|
15
|
-
console.warn('Failed to
|
|
18
|
+
console.warn('Failed to stop persistence agent:', e);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
// 2.
|
|
21
|
+
// 2. Find any remaining processes still using the port
|
|
19
22
|
let pids: string[] = [];
|
|
20
23
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
if (isWindows) {
|
|
25
|
+
// netstat -ano lists PID in the last column; filter by :<port> with LISTENING or ESTABLISHED
|
|
26
|
+
const output = execSync(`netstat -ano | findstr :${port}`).toString();
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const line of output.trim().split('\n')) {
|
|
29
|
+
const parts = line.trim().split(/\s+/);
|
|
30
|
+
const pid = parts[parts.length - 1];
|
|
31
|
+
if (pid && /^\d+$/.test(pid) && pid !== '0' && !seen.has(pid)) {
|
|
32
|
+
seen.add(pid);
|
|
33
|
+
pids.push(pid);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
pids = execSync(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
24
40
|
pids = [];
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
if (pids.length === 0) {
|
|
28
|
-
console.log(`No process found using port ${port}
|
|
44
|
+
console.log(`No process found using port ${port}.`);
|
|
29
45
|
return;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
|
-
// 3.
|
|
48
|
+
// 3. Kill each process
|
|
33
49
|
for (const pid of pids) {
|
|
34
50
|
try {
|
|
35
|
-
|
|
51
|
+
if (isWindows) {
|
|
52
|
+
execSync(`taskkill /PID ${pid} /F`, { stdio: 'pipe' });
|
|
53
|
+
} else {
|
|
54
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
55
|
+
}
|
|
36
56
|
console.log(`Killed process with PID ${pid} using port ${port}.`);
|
|
37
57
|
} catch (e) {
|
|
38
58
|
console.error(`Failed to kill process ${pid}:`, e);
|
package/src/onboard.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { getConfigDir, getConfigPath } from './utils';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
|
|
@@ -8,8 +9,7 @@ import path from 'path';
|
|
|
8
9
|
*/
|
|
9
10
|
export async function onboard(openAiKey?: string) {
|
|
10
11
|
let apiKey = openAiKey;
|
|
11
|
-
const
|
|
12
|
-
const configDir = path.join(homeDir, '.omnikey');
|
|
12
|
+
const configDir = getConfigDir();
|
|
13
13
|
const sqlitePath = path.join(configDir, 'omnikey-selfhosted.sqlite');
|
|
14
14
|
|
|
15
15
|
if (!apiKey) {
|
|
@@ -25,7 +25,7 @@ export async function onboard(openAiKey?: string) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Save all environment variables to ~/.omnikey/config.json
|
|
28
|
-
const configPath =
|
|
28
|
+
const configPath = getConfigPath();
|
|
29
29
|
fs.mkdirSync(configDir, { recursive: true });
|
|
30
30
|
const configVars = {
|
|
31
31
|
OPENAI_API_KEY: apiKey,
|
package/src/removeConfig.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import { execSync } from 'child_process';
|
|
4
|
+
import { isWindows, getHomeDir, getConfigDir, readConfig } from './utils';
|
|
5
5
|
|
|
6
6
|
export function killLaunchdAgent() {
|
|
7
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
8
7
|
const plistName = 'com.omnikey.daemon.plist';
|
|
9
|
-
const plistPath = path.join(
|
|
8
|
+
const plistPath = path.join(getHomeDir(), 'Library', 'LaunchAgents', plistName);
|
|
10
9
|
if (fs.existsSync(plistPath)) {
|
|
11
10
|
try {
|
|
12
11
|
execSync(`launchctl unload "${plistPath}"`);
|
|
@@ -20,31 +19,59 @@ export function killLaunchdAgent() {
|
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
export function killWindowsTask() {
|
|
23
|
+
const taskName = 'OmnikeyDaemon';
|
|
24
|
+
try {
|
|
25
|
+
execSync(`schtasks /end /tn "${taskName}"`, { stdio: 'pipe' });
|
|
26
|
+
} catch {
|
|
27
|
+
// Task may not be running — that's fine
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
31
|
+
console.log(`Removed Windows Task Scheduler task: ${taskName}`);
|
|
32
|
+
} catch {
|
|
33
|
+
console.log(`Windows Task Scheduler task does not exist: ${taskName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Also remove the wrapper script
|
|
37
|
+
const wrapperPath = path.join(getConfigDir(), 'start-daemon.cmd');
|
|
38
|
+
if (fs.existsSync(wrapperPath)) {
|
|
39
|
+
try {
|
|
40
|
+
fs.rmSync(wrapperPath);
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Kill the platform-appropriate persistence agent (launchd on macOS, Task Scheduler on Windows).
|
|
49
|
+
*/
|
|
50
|
+
export function killPersistenceAgent() {
|
|
51
|
+
if (isWindows) {
|
|
52
|
+
killWindowsTask();
|
|
53
|
+
} else {
|
|
54
|
+
killLaunchdAgent();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
/**
|
|
24
59
|
* Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
|
|
25
60
|
*/
|
|
26
61
|
export function removeConfigAndDb() {
|
|
27
|
-
const homeDir =
|
|
28
|
-
const configDir =
|
|
29
|
-
const
|
|
30
|
-
let sqlitePath = path.join(homeDir, 'omnikey-selfhosted.sqlite');
|
|
62
|
+
const homeDir = getHomeDir();
|
|
63
|
+
const configDir = getConfigDir();
|
|
64
|
+
const configData = readConfig();
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sqlitePath = path.isAbsolute(configData.SQLITE_PATH)
|
|
38
|
-
? configData.SQLITE_PATH
|
|
39
|
-
: path.join(homeDir, configData.SQLITE_PATH);
|
|
40
|
-
}
|
|
41
|
-
} catch (e) {
|
|
42
|
-
console.error(`Failed to read config.json: ${e}`);
|
|
43
|
-
}
|
|
66
|
+
let sqlitePath = path.join(homeDir, 'omnikey-selfhosted.sqlite');
|
|
67
|
+
if (configData.SQLITE_PATH) {
|
|
68
|
+
sqlitePath = path.isAbsolute(configData.SQLITE_PATH)
|
|
69
|
+
? configData.SQLITE_PATH
|
|
70
|
+
: path.join(homeDir, configData.SQLITE_PATH);
|
|
44
71
|
}
|
|
45
72
|
|
|
46
|
-
// Remove
|
|
47
|
-
|
|
73
|
+
// Remove platform-appropriate persistence agent
|
|
74
|
+
killPersistenceAgent();
|
|
48
75
|
|
|
49
76
|
// Remove SQLite database
|
|
50
77
|
if (fs.existsSync(sqlitePath)) {
|
package/src/showLogs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { getConfigDir } from './utils';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Show the logs of the running Omnikey daemon by printing the contents of the daemon log file.
|
|
@@ -7,9 +8,7 @@ import path from 'path';
|
|
|
7
8
|
* If errorsOnly is true, shows daemon-error.log instead.
|
|
8
9
|
*/
|
|
9
10
|
export function showLogs(lines: number = 50, errorsOnly: boolean = false) {
|
|
10
|
-
const
|
|
11
|
-
const configDir = path.join(homeDir, '.omnikey');
|
|
12
|
-
const logPath = path.join(configDir, errorsOnly ? 'daemon-error.log' : 'daemon.log');
|
|
11
|
+
const logPath = path.join(getConfigDir(), errorsOnly ? 'daemon-error.log' : 'daemon.log');
|
|
13
12
|
|
|
14
13
|
if (!fs.existsSync(logPath)) {
|
|
15
14
|
console.log(errorsOnly ? 'No error logs found.' : 'No daemon logs found.');
|
package/src/status.ts
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
1
|
import { execSync } from 'child_process';
|
|
2
|
+
import { isWindows, getPort } from './utils';
|
|
4
3
|
|
|
5
4
|
export function statusCmd() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const configDir = path.join(homeDir, '.omnikey');
|
|
9
|
-
const configPath = path.join(configDir, 'config.json');
|
|
10
|
-
let port = 7071;
|
|
11
|
-
if (fs.existsSync(configPath)) {
|
|
12
|
-
try {
|
|
13
|
-
const configVars = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
14
|
-
if (configVars.OMNIKEY_PORT) {
|
|
15
|
-
port = Number(configVars.OMNIKEY_PORT);
|
|
16
|
-
}
|
|
17
|
-
} catch (e) {
|
|
18
|
-
console.error('Failed to read config.json:', e);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
5
|
+
const port = getPort();
|
|
6
|
+
|
|
21
7
|
try {
|
|
22
|
-
|
|
8
|
+
let output: string;
|
|
9
|
+
if (isWindows) {
|
|
10
|
+
output = execSync(`netstat -ano | findstr :${port}`).toString();
|
|
11
|
+
} else {
|
|
12
|
+
output = execSync(`lsof -i :${port}`).toString();
|
|
13
|
+
}
|
|
23
14
|
if (output.trim()) {
|
|
24
15
|
console.log(`Processes using port ${port}:\n${output}`);
|
|
25
16
|
} else {
|
|
26
17
|
console.log(`No process is using port ${port}.`);
|
|
27
18
|
}
|
|
28
|
-
} catch
|
|
19
|
+
} catch {
|
|
29
20
|
console.log(`No process is using port ${port}.`);
|
|
30
21
|
}
|
|
31
22
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export const isWindows = process.platform === 'win32';
|
|
6
|
+
|
|
7
|
+
export function getHomeDir(): string {
|
|
8
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getConfigDir(): string {
|
|
12
|
+
return path.join(getHomeDir(), '.omnikey');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getConfigPath(): string {
|
|
16
|
+
return path.join(getConfigDir(), 'config.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readConfig(): Record<string, any> {
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
24
|
+
} catch {
|
|
25
|
+
// fall through to empty config
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPort(): number {
|
|
32
|
+
const config = readConfig();
|
|
33
|
+
return config.OMNIKEY_PORT ? Number(config.OMNIKEY_PORT) : 7071;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initLogFiles(logPath: string, errorLogPath: string): { out: number; err: number } {
|
|
37
|
+
try {
|
|
38
|
+
fs.writeFileSync(logPath, '');
|
|
39
|
+
fs.writeFileSync(errorLogPath, '');
|
|
40
|
+
} catch {
|
|
41
|
+
// Ignore if files don't exist yet
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
out: fs.openSync(logPath, 'a'),
|
|
45
|
+
err: fs.openSync(errorLogPath, 'a'),
|
|
46
|
+
};
|
|
47
|
+
}
|