omnikey-cli 1.5.7 → 1.6.0
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/__tests__/ai-client.nemotron.test.js +127 -0
- package/backend-dist/agent/agentPrompts.js +4 -3
- package/backend-dist/agent/utils.js +6 -5
- package/backend-dist/ai-client.js +151 -16
- package/backend-dist/config.js +16 -1
- package/backend-dist/db.js +5 -1
- package/backend-dist/mcpServerRoutes.js +16 -4
- package/backend-dist/scheduledJobRoutes.js +5 -2
- package/dist/index.js +1 -1
- package/dist/onboard.js +38 -0
- package/dist/telegramClient.js +1 -1
- package/dist/telegramDaemon.js +6 -4
- package/package.json +8 -6
- package/src/index.ts +1 -1
- package/src/onboard.ts +38 -0
- package/src/telegramClient.ts +1 -1
- package/src/telegramDaemon.ts +6 -8
- package/telegram-client-dist/{dist/agentClient.js → agentClient.js} +91 -75
- package/telegram-client-dist/{dist/config.js → config.js} +10 -12
- package/telegram-client-dist/{dist/index.js → index.js} +23 -32
- package/telegram-client-dist/{dist/notifyTelegram.js → notifyTelegram.js} +193 -194
- package/telegram-client-dist/{dist/omnikeyAuth.js → omnikeyAuth.js} +8 -13
- package/telegram-client-dist/dist/db.js +0 -78
package/dist/telegramDaemon.js
CHANGED
|
@@ -20,12 +20,12 @@ const LABEL = `com.${os_1.default.userInfo().username}.telegram`;
|
|
|
20
20
|
const PLIST_NAME = `${LABEL}.plist`;
|
|
21
21
|
const WINDOWS_SERVICE_NAME = 'OmnikeyTelegram';
|
|
22
22
|
// At runtime __dirname is cli/dist/. The bundled telegram app is copied into
|
|
23
|
-
// cli/telegram-client-dist/ by the build:telegram-client script
|
|
24
|
-
// up from dist/ lands at the package
|
|
23
|
+
// cli/telegram-client-dist/ by the build:telegram-client script (flat layout,
|
|
24
|
+
// matching backend-dist/), so one level up from dist/ lands at the package
|
|
25
25
|
// This matches resolveBundleRoot() in telegramClient.ts and works correctly
|
|
26
26
|
// both in the monorepo and after `npm install -g omnikey-cli`.
|
|
27
27
|
const TELEGRAM_BOT_ROOT = path_1.default.resolve(__dirname, '..', 'telegram-client-dist');
|
|
28
|
-
const ENTRY_POINT = path_1.default.join(TELEGRAM_BOT_ROOT, '
|
|
28
|
+
const ENTRY_POINT = path_1.default.join(TELEGRAM_BOT_ROOT, 'index.js');
|
|
29
29
|
const HOME = (0, utils_1.getHomeDir)();
|
|
30
30
|
// macOS — launchd LaunchAgent paths
|
|
31
31
|
const LAUNCH_AGENTS_DIR = path_1.default.join(HOME, 'Library', 'LaunchAgents');
|
|
@@ -289,7 +289,9 @@ async function startWindows() {
|
|
|
289
289
|
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'Start', 'SERVICE_AUTO_START'], {
|
|
290
290
|
stdio: 'pipe',
|
|
291
291
|
});
|
|
292
|
-
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'DisplayName', 'Omnikey Telegram'], {
|
|
292
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'DisplayName', 'Omnikey Telegram'], {
|
|
293
|
+
stdio: 'pipe',
|
|
294
|
+
});
|
|
293
295
|
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'Description', 'Omnikey Telegram Daemon'], { stdio: 'pipe' });
|
|
294
296
|
(0, child_process_1.execFileSync)(nssmPath, ['start', WINDOWS_SERVICE_NAME], { stdio: 'pipe' });
|
|
295
297
|
console.log(`NSSM service installed and started: ${WINDOWS_SERVICE_NAME}`);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.6.0",
|
|
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",
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
"omnikey": "dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc &&
|
|
17
|
+
"build": "tsc && yarn run copy-backend && yarn run build:telegram-client",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
|
-
"copy-backend": "rm -rf backend-dist && mkdir -p backend-dist && cp -R ../dist/* backend-dist/",
|
|
20
|
-
"build:telegram-client": "rm -rf telegram-client-dist && mkdir -p telegram-client-dist
|
|
19
|
+
"copy-backend": "rm -rf backend-dist && mkdir -p backend-dist && cp -R ../api/dist/* backend-dist/",
|
|
20
|
+
"build:telegram-client": "rm -rf telegram-client-dist && mkdir -p telegram-client-dist && cp -R ../telegram/dist/* telegram-client-dist/",
|
|
21
|
+
"clean": "rm -rf dist backend-dist telegram-client-dist",
|
|
22
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
23
|
+
"test": "echo \"(no tests in this workspace)\" && exit 0",
|
|
24
|
+
"lint": "echo \"(no lint in this workspace)\" && exit 0"
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"cli",
|
|
@@ -33,7 +37,6 @@
|
|
|
33
37
|
"@google/genai": "^1.46.0",
|
|
34
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
39
|
"axios": "^1.13.5",
|
|
36
|
-
"better-sqlite3": "^12.10.0",
|
|
37
40
|
"commander": "^11.0.0",
|
|
38
41
|
"cors": "^2.8.5",
|
|
39
42
|
"cron-parser": "^4.9.0",
|
|
@@ -54,7 +57,6 @@
|
|
|
54
57
|
"zod": "^4.3.6"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
58
60
|
"@types/inquirer": "^9.0.9",
|
|
59
61
|
"@types/node-telegram-bot-api": "^0.64.0",
|
|
60
62
|
"typescript": "^5.0.0"
|
package/src/index.ts
CHANGED
package/src/onboard.ts
CHANGED
|
@@ -7,6 +7,10 @@ const AI_PROVIDERS = [
|
|
|
7
7
|
{ name: 'OpenAI (gpt-4o-mini / gpt-5.5)', value: 'openai' },
|
|
8
8
|
{ name: 'Anthropic — Claude (claude-haiku / claude-opus)', value: 'anthropic' },
|
|
9
9
|
{ name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
|
|
10
|
+
{
|
|
11
|
+
name: 'NVIDIA Nemotron (nemotron-3-nano / nemotron-3-ultra) — open weights',
|
|
12
|
+
value: 'nemotron',
|
|
13
|
+
},
|
|
10
14
|
];
|
|
11
15
|
|
|
12
16
|
const SEARCH_PROVIDERS = [
|
|
@@ -22,12 +26,14 @@ const AI_PROVIDER_KEY_ENV: Record<string, string> = {
|
|
|
22
26
|
openai: 'OPENAI_API_KEY',
|
|
23
27
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
24
28
|
gemini: 'GEMINI_API_KEY',
|
|
29
|
+
nemotron: 'NVIDIA_API_KEY',
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
const AI_PROVIDER_KEY_LABEL: Record<string, string> = {
|
|
28
33
|
openai: 'OpenAI API key (from platform.openai.com)',
|
|
29
34
|
anthropic: 'Anthropic API key (from console.anthropic.com)',
|
|
30
35
|
gemini: 'Google Gemini API key (from ai.google.dev)',
|
|
36
|
+
nemotron: 'NVIDIA API key (from build.nvidia.com — used by NIM/Nemotron)',
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -57,6 +63,37 @@ export async function onboard() {
|
|
|
57
63
|
},
|
|
58
64
|
]);
|
|
59
65
|
|
|
66
|
+
// Provider-specific extras
|
|
67
|
+
const providerExtras: Record<string, string> = {};
|
|
68
|
+
|
|
69
|
+
if (aiProvider === 'nemotron') {
|
|
70
|
+
// Nemotron is served either via the public NVIDIA NIM gateway or a
|
|
71
|
+
// self-hosted NIM microservice. Always ask the user for the base URL so
|
|
72
|
+
// they can point at either. The default value matches the public gateway
|
|
73
|
+
// so pressing Enter "just works" for build.nvidia.com keys.
|
|
74
|
+
const DEFAULT_NEMOTRON_URL = 'https://integrate.api.nvidia.com/v1';
|
|
75
|
+
const { nemotronBaseUrl } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
type: 'input',
|
|
78
|
+
name: 'nemotronBaseUrl',
|
|
79
|
+
message: 'Enter the Nemotron / NVIDIA NIM base URL (press Enter for the public gateway):',
|
|
80
|
+
default: DEFAULT_NEMOTRON_URL,
|
|
81
|
+
validate: (input: string) => {
|
|
82
|
+
const trimmed = input.trim();
|
|
83
|
+
if (trimmed === '') return 'URL cannot be empty';
|
|
84
|
+
try {
|
|
85
|
+
// eslint-disable-next-line no-new
|
|
86
|
+
new URL(trimmed);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return 'Please enter a valid URL (including the scheme, e.g. https://...)';
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
providerExtras['NEMOTRON_BASE_URL'] = nemotronBaseUrl.trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
// Web search provider (optional)
|
|
61
98
|
const { provider } = await inquirer.prompt([
|
|
62
99
|
{
|
|
@@ -122,6 +159,7 @@ export async function onboard() {
|
|
|
122
159
|
[AI_PROVIDER_KEY_ENV[aiProvider]]: apiKey,
|
|
123
160
|
IS_SELF_HOSTED: true,
|
|
124
161
|
SQLITE_PATH: sqlitePath,
|
|
162
|
+
...providerExtras,
|
|
125
163
|
...searchConfig,
|
|
126
164
|
};
|
|
127
165
|
fs.writeFileSync(configPath, JSON.stringify(configVars, null, 2));
|
package/src/telegramClient.ts
CHANGED
|
@@ -24,7 +24,7 @@ function resolveBundleRoot(): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function resolveBundledEntry(): string {
|
|
27
|
-
return path.join(resolveBundleRoot(), '
|
|
27
|
+
return path.join(resolveBundleRoot(), 'index.js');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function persistConfig(values: Record<string, string>): void {
|
package/src/telegramDaemon.ts
CHANGED
|
@@ -11,12 +11,12 @@ const PLIST_NAME = `${LABEL}.plist`;
|
|
|
11
11
|
const WINDOWS_SERVICE_NAME = 'OmnikeyTelegram';
|
|
12
12
|
|
|
13
13
|
// At runtime __dirname is cli/dist/. The bundled telegram app is copied into
|
|
14
|
-
// cli/telegram-client-dist/ by the build:telegram-client script
|
|
15
|
-
// up from dist/ lands at the package
|
|
14
|
+
// cli/telegram-client-dist/ by the build:telegram-client script (flat layout,
|
|
15
|
+
// matching backend-dist/), so one level up from dist/ lands at the package
|
|
16
16
|
// This matches resolveBundleRoot() in telegramClient.ts and works correctly
|
|
17
17
|
// both in the monorepo and after `npm install -g omnikey-cli`.
|
|
18
18
|
const TELEGRAM_BOT_ROOT = path.resolve(__dirname, '..', 'telegram-client-dist');
|
|
19
|
-
const ENTRY_POINT = path.join(TELEGRAM_BOT_ROOT, '
|
|
19
|
+
const ENTRY_POINT = path.join(TELEGRAM_BOT_ROOT, 'index.js');
|
|
20
20
|
|
|
21
21
|
const HOME = getHomeDir();
|
|
22
22
|
|
|
@@ -300,11 +300,9 @@ async function startWindows(): Promise<void> {
|
|
|
300
300
|
execFileSync(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'Start', 'SERVICE_AUTO_START'], {
|
|
301
301
|
stdio: 'pipe',
|
|
302
302
|
});
|
|
303
|
-
execFileSync(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
{ stdio: 'pipe' },
|
|
307
|
-
);
|
|
303
|
+
execFileSync(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'DisplayName', 'Omnikey Telegram'], {
|
|
304
|
+
stdio: 'pipe',
|
|
305
|
+
});
|
|
308
306
|
execFileSync(
|
|
309
307
|
nssmPath,
|
|
310
308
|
['set', WINDOWS_SERVICE_NAME, 'Description', 'Omnikey Telegram Daemon'],
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AgentAbortError = void 0;
|
|
7
|
+
exports.getSessionMessages = getSessionMessages;
|
|
7
8
|
exports.listRecentSessions = listRecentSessions;
|
|
8
9
|
exports.listTaskTemplates = listTaskTemplates;
|
|
9
10
|
exports.setDefaultTaskTemplate = setDefaultTaskTemplate;
|
|
@@ -18,6 +19,27 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
19
|
const crypto_1 = require("crypto");
|
|
19
20
|
const config_1 = require("./config");
|
|
20
21
|
const omnikeyAuth_1 = require("./omnikeyAuth");
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the typed message transcript for a session.
|
|
24
|
+
* Returns null when the session does not exist (404), throws on other errors.
|
|
25
|
+
*/
|
|
26
|
+
async function getSessionMessages(logger, sessionId) {
|
|
27
|
+
const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
|
|
28
|
+
const url = `${(0, config_1.omnikeyBaseUrl)()}/api/agent/sessions/${encodeURIComponent(sessionId)}/messages`;
|
|
29
|
+
try {
|
|
30
|
+
const resp = await axios_1.default.get(url, {
|
|
31
|
+
timeout: 10000,
|
|
32
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
33
|
+
});
|
|
34
|
+
return resp.data?.messages ?? [];
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (axios_1.default.isAxiosError(err) && err.response?.status === 404) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
async function listRecentSessions(logger, limit = 5) {
|
|
22
44
|
const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
|
|
23
45
|
const url = `${(0, config_1.omnikeyBaseUrl)()}/api/agent/sessions`;
|
|
@@ -48,7 +70,7 @@ async function setDefaultTaskTemplate(logger, templateId) {
|
|
|
48
70
|
timeout: 10000,
|
|
49
71
|
headers: { Authorization: `Bearer ${token}` },
|
|
50
72
|
});
|
|
51
|
-
logger.info(
|
|
73
|
+
logger.info('Set default task template', { templateId });
|
|
52
74
|
}
|
|
53
75
|
async function listProjectGroups(logger) {
|
|
54
76
|
const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
|
|
@@ -60,67 +82,63 @@ async function listProjectGroups(logger) {
|
|
|
60
82
|
return resp.data?.groups ?? [];
|
|
61
83
|
}
|
|
62
84
|
class AgentAbortError extends Error {
|
|
63
|
-
constructor(message =
|
|
85
|
+
constructor(message = 'Agent run aborted') {
|
|
64
86
|
super(message);
|
|
65
|
-
this.name =
|
|
87
|
+
this.name = 'AgentAbortError';
|
|
66
88
|
}
|
|
67
89
|
}
|
|
68
90
|
exports.AgentAbortError = AgentAbortError;
|
|
69
91
|
function extractTagged(content, tag) {
|
|
70
|
-
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`,
|
|
92
|
+
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
|
|
71
93
|
const m = content.match(re);
|
|
72
94
|
return m?.[1]?.trim() || null;
|
|
73
95
|
}
|
|
74
96
|
function stripTagged(content, tag) {
|
|
75
|
-
return content.replace(new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`,
|
|
97
|
+
return content.replace(new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'), '');
|
|
76
98
|
}
|
|
77
99
|
function cleanReasoning(content) {
|
|
78
100
|
return content
|
|
79
|
-
.replace(/<\/?shell_function_calls>/gi,
|
|
80
|
-
.replace(/<final_answer>([\s\S]*?)<\/final_answer>/gi,
|
|
101
|
+
.replace(/<\/?shell_function_calls>/gi, '')
|
|
102
|
+
.replace(/<final_answer>([\s\S]*?)<\/final_answer>/gi, '$1')
|
|
81
103
|
.trim();
|
|
82
104
|
}
|
|
83
105
|
const SHELL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
84
106
|
const SHELL_OUTPUT_MAX = 64 * 1024;
|
|
85
107
|
// Mirrors WINDOWS_SHELL_CANDIDATES in src/agent/mcpRuntime.ts
|
|
86
108
|
const WINDOWS_SHELL_CANDIDATES = [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
110
|
+
'C:\\Program Files\\PowerShell\\6\\pwsh.exe',
|
|
111
|
+
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
|
112
|
+
'C:\\Windows\\System32\\cmd.exe',
|
|
113
|
+
'C:\\Windows\\cmd.exe',
|
|
92
114
|
];
|
|
93
115
|
// Resolve the Windows shell: COMSPEC → SystemRoot\System32\cmd.exe → candidate list.
|
|
94
116
|
// Mirrors resolveLoginShell() in src/agent/mcpRuntime.ts, with SystemRoot used to
|
|
95
117
|
// locate cmd.exe from the Win32 system root rather than a hardcoded drive letter.
|
|
96
118
|
function resolveWindowsShell() {
|
|
97
|
-
const comspec = process.env.COMSPEC ??
|
|
119
|
+
const comspec = process.env.COMSPEC ?? '';
|
|
98
120
|
if (comspec && (0, fs_1.existsSync)(comspec))
|
|
99
121
|
return comspec;
|
|
100
|
-
const systemRoot = process.env.SystemRoot ??
|
|
101
|
-
const cmdFromRoot = path_1.default.join(systemRoot,
|
|
122
|
+
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
123
|
+
const cmdFromRoot = path_1.default.join(systemRoot, 'System32', 'cmd.exe');
|
|
102
124
|
if ((0, fs_1.existsSync)(cmdFromRoot))
|
|
103
125
|
return cmdFromRoot;
|
|
104
126
|
for (const candidate of WINDOWS_SHELL_CANDIDATES) {
|
|
105
127
|
if ((0, fs_1.existsSync)(candidate))
|
|
106
128
|
return candidate;
|
|
107
129
|
}
|
|
108
|
-
return
|
|
130
|
+
return 'cmd.exe';
|
|
109
131
|
}
|
|
110
132
|
// Build shell args for the resolved shell — mirrors wrapWithLoginShell() in
|
|
111
133
|
// src/agent/mcpRuntime.ts. PowerShell/pwsh use -NoProfile -Command; cmd uses /c.
|
|
112
134
|
function buildWindowsShellArgs(shell, script) {
|
|
113
135
|
const name = path_1.default.basename(shell).toLowerCase();
|
|
114
|
-
if (name ===
|
|
115
|
-
return [
|
|
136
|
+
if (name === 'pwsh.exe' || name === 'powershell.exe') {
|
|
137
|
+
return ['-NoProfile', '-Command', script];
|
|
116
138
|
}
|
|
117
|
-
return [
|
|
139
|
+
return ['/c', script];
|
|
118
140
|
}
|
|
119
|
-
const PLATFORM = process.platform ===
|
|
120
|
-
? "windows"
|
|
121
|
-
: process.platform === "darwin"
|
|
122
|
-
? "macos"
|
|
123
|
-
: "linux";
|
|
141
|
+
const PLATFORM = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
|
|
124
142
|
/**
|
|
125
143
|
* Execute a shell script locally and capture combined stdout+stderr.
|
|
126
144
|
* On macOS/Linux: invoke the login shell with `-l -c <script>` (mirrors the
|
|
@@ -134,15 +152,15 @@ function runShellScript(script, logger) {
|
|
|
134
152
|
return new Promise((resolve) => {
|
|
135
153
|
let shell;
|
|
136
154
|
let shellArgs;
|
|
137
|
-
if (process.platform !==
|
|
155
|
+
if (process.platform !== 'darwin' && process.platform === 'win32') {
|
|
138
156
|
shell = resolveWindowsShell();
|
|
139
157
|
shellArgs = buildWindowsShellArgs(shell, script);
|
|
140
158
|
}
|
|
141
159
|
else {
|
|
142
|
-
shell = process.env.SHELL ||
|
|
143
|
-
shellArgs = [
|
|
160
|
+
shell = process.env.SHELL || '/bin/zsh';
|
|
161
|
+
shellArgs = ['-l', '-c', script];
|
|
144
162
|
}
|
|
145
|
-
logger.info(
|
|
163
|
+
logger.info('Executing shell script from agent', {
|
|
146
164
|
shell,
|
|
147
165
|
platform: PLATFORM,
|
|
148
166
|
length: script.length,
|
|
@@ -151,7 +169,7 @@ function runShellScript(script, logger) {
|
|
|
151
169
|
cwd: process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(),
|
|
152
170
|
env: process.env,
|
|
153
171
|
});
|
|
154
|
-
let buf =
|
|
172
|
+
let buf = '';
|
|
155
173
|
let truncated = false;
|
|
156
174
|
const append = (chunk) => {
|
|
157
175
|
if (truncated)
|
|
@@ -161,7 +179,7 @@ function runShellScript(script, logger) {
|
|
|
161
179
|
truncated = true;
|
|
162
180
|
return;
|
|
163
181
|
}
|
|
164
|
-
const text = chunk.toString(
|
|
182
|
+
const text = chunk.toString('utf8');
|
|
165
183
|
if (text.length <= room) {
|
|
166
184
|
buf += text;
|
|
167
185
|
}
|
|
@@ -170,33 +188,31 @@ function runShellScript(script, logger) {
|
|
|
170
188
|
truncated = true;
|
|
171
189
|
}
|
|
172
190
|
};
|
|
173
|
-
child.stdout.on(
|
|
174
|
-
child.stderr.on(
|
|
191
|
+
child.stdout.on('data', append);
|
|
192
|
+
child.stderr.on('data', append);
|
|
175
193
|
const timeout = setTimeout(() => {
|
|
176
|
-
logger.warn(
|
|
194
|
+
logger.warn('Shell script timed out; sending SIGTERM', {
|
|
177
195
|
timeoutMs: SHELL_TIMEOUT_MS,
|
|
178
196
|
});
|
|
179
197
|
try {
|
|
180
|
-
child.kill(
|
|
198
|
+
child.kill('SIGTERM');
|
|
181
199
|
}
|
|
182
200
|
catch {
|
|
183
201
|
/* noop */
|
|
184
202
|
}
|
|
185
203
|
}, SHELL_TIMEOUT_MS);
|
|
186
|
-
child.on(
|
|
204
|
+
child.on('error', (err) => {
|
|
187
205
|
clearTimeout(timeout);
|
|
188
206
|
resolve({
|
|
189
207
|
output: `${buf}\n[shell spawn error: ${err.message}]`,
|
|
190
208
|
status: -1,
|
|
191
209
|
});
|
|
192
210
|
});
|
|
193
|
-
child.on(
|
|
211
|
+
child.on('close', (code, signal) => {
|
|
194
212
|
clearTimeout(timeout);
|
|
195
|
-
const status = typeof code ===
|
|
196
|
-
const finalOutput = truncated
|
|
197
|
-
|
|
198
|
-
: buf;
|
|
199
|
-
logger.info("Shell script finished", {
|
|
213
|
+
const status = typeof code === 'number' ? code : signal ? 1 : 0;
|
|
214
|
+
const finalOutput = truncated ? `${buf}\n... [truncated to ${SHELL_OUTPUT_MAX} bytes]` : buf;
|
|
215
|
+
logger.info('Shell script finished', {
|
|
200
216
|
status,
|
|
201
217
|
signal,
|
|
202
218
|
outputLength: finalOutput.length,
|
|
@@ -217,7 +233,7 @@ function runShellScript(script, logger) {
|
|
|
217
233
|
async function runAgentTurn(logger, opts) {
|
|
218
234
|
const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
|
|
219
235
|
const sessionId = opts.sessionId || (0, crypto_1.randomUUID)();
|
|
220
|
-
const url = (0, config_1.omnikeyWsUrl)(
|
|
236
|
+
const url = (0, config_1.omnikeyWsUrl)('/ws/omni-agent');
|
|
221
237
|
return new Promise((resolve, reject) => {
|
|
222
238
|
if (opts.signal?.aborted) {
|
|
223
239
|
reject(new AgentAbortError());
|
|
@@ -232,7 +248,7 @@ async function runAgentTurn(logger, opts) {
|
|
|
232
248
|
return;
|
|
233
249
|
settled = true;
|
|
234
250
|
if (opts.signal && onAbort) {
|
|
235
|
-
opts.signal.removeEventListener(
|
|
251
|
+
opts.signal.removeEventListener('abort', onAbort);
|
|
236
252
|
}
|
|
237
253
|
try {
|
|
238
254
|
ws.close();
|
|
@@ -247,12 +263,12 @@ async function runAgentTurn(logger, opts) {
|
|
|
247
263
|
};
|
|
248
264
|
const onAbort = opts.signal
|
|
249
265
|
? () => {
|
|
250
|
-
logger.info(
|
|
266
|
+
logger.info('Agent run aborted by caller', { sessionId });
|
|
251
267
|
finish(new AgentAbortError());
|
|
252
268
|
}
|
|
253
269
|
: null;
|
|
254
270
|
if (opts.signal && onAbort) {
|
|
255
|
-
opts.signal.addEventListener(
|
|
271
|
+
opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
256
272
|
}
|
|
257
273
|
const send = (msg) => {
|
|
258
274
|
ws.send(JSON.stringify(msg), (err) => {
|
|
@@ -260,11 +276,11 @@ async function runAgentTurn(logger, opts) {
|
|
|
260
276
|
finish(err);
|
|
261
277
|
});
|
|
262
278
|
};
|
|
263
|
-
ws.on(
|
|
264
|
-
logger.info(
|
|
279
|
+
ws.on('open', () => {
|
|
280
|
+
logger.info('Agent WebSocket open', { sessionId });
|
|
265
281
|
send({
|
|
266
282
|
session_id: sessionId,
|
|
267
|
-
sender:
|
|
283
|
+
sender: 'client',
|
|
268
284
|
content: opts.prompt,
|
|
269
285
|
is_terminal_output: false,
|
|
270
286
|
is_error: false,
|
|
@@ -272,56 +288,56 @@ async function runAgentTurn(logger, opts) {
|
|
|
272
288
|
group_name: opts.groupName,
|
|
273
289
|
});
|
|
274
290
|
});
|
|
275
|
-
ws.on(
|
|
291
|
+
ws.on('message', async (data) => {
|
|
276
292
|
let msg;
|
|
277
293
|
try {
|
|
278
294
|
msg = JSON.parse(data.toString());
|
|
279
295
|
}
|
|
280
296
|
catch (e) {
|
|
281
|
-
logger.warn(
|
|
297
|
+
logger.warn('Failed to parse agent ws message', {
|
|
282
298
|
error: e.message,
|
|
283
299
|
});
|
|
284
300
|
return;
|
|
285
301
|
}
|
|
286
|
-
const content = msg.content ||
|
|
302
|
+
const content = msg.content || '';
|
|
287
303
|
if (msg.is_error) {
|
|
288
|
-
finish(new Error(content ||
|
|
304
|
+
finish(new Error(content || 'Agent reported an error'));
|
|
289
305
|
return;
|
|
290
306
|
}
|
|
291
307
|
if (msg.is_web_call) {
|
|
292
|
-
await opts.onBlock({ kind:
|
|
308
|
+
await opts.onBlock({ kind: 'webCall', text: content });
|
|
293
309
|
return;
|
|
294
310
|
}
|
|
295
311
|
if (msg.is_image_rendering) {
|
|
296
|
-
await opts.onBlock({ kind:
|
|
312
|
+
await opts.onBlock({ kind: 'imageRendering', text: content });
|
|
297
313
|
return;
|
|
298
314
|
}
|
|
299
315
|
if (msg.is_mcp_call) {
|
|
300
|
-
await opts.onBlock({ kind:
|
|
316
|
+
await opts.onBlock({ kind: 'mcpCall', text: content });
|
|
301
317
|
return;
|
|
302
318
|
}
|
|
303
|
-
const finalAnswer = extractTagged(content,
|
|
319
|
+
const finalAnswer = extractTagged(content, 'final_answer');
|
|
304
320
|
if (finalAnswer) {
|
|
305
|
-
await opts.onBlock({ kind:
|
|
321
|
+
await opts.onBlock({ kind: 'finalAnswer', text: finalAnswer });
|
|
306
322
|
finish(null, { sessionId, finalAnswer });
|
|
307
323
|
return;
|
|
308
324
|
}
|
|
309
|
-
const shellScript = extractTagged(content,
|
|
325
|
+
const shellScript = extractTagged(content, 'shell_script');
|
|
310
326
|
if (shellScript) {
|
|
311
|
-
const reasoning = cleanReasoning(stripTagged(content,
|
|
327
|
+
const reasoning = cleanReasoning(stripTagged(content, 'shell_script'));
|
|
312
328
|
if (reasoning)
|
|
313
|
-
await opts.onBlock({ kind:
|
|
314
|
-
await opts.onBlock({ kind:
|
|
329
|
+
await opts.onBlock({ kind: 'reasoning', text: reasoning });
|
|
330
|
+
await opts.onBlock({ kind: 'shellCommand', text: shellScript });
|
|
315
331
|
try {
|
|
316
332
|
const { output, status } = await runShellScript(shellScript, logger);
|
|
317
|
-
const statusLabel = status === 0 ?
|
|
333
|
+
const statusLabel = status === 0 ? 'success' : `error (exit code: ${status})`;
|
|
318
334
|
await opts.onBlock({
|
|
319
|
-
kind:
|
|
335
|
+
kind: 'terminalOutput',
|
|
320
336
|
text: `[terminal ${statusLabel}]\n${output}`,
|
|
321
337
|
});
|
|
322
338
|
send({
|
|
323
339
|
session_id: sessionId,
|
|
324
|
-
sender:
|
|
340
|
+
sender: 'client',
|
|
325
341
|
content: output,
|
|
326
342
|
is_terminal_output: true,
|
|
327
343
|
is_error: status !== 0,
|
|
@@ -330,14 +346,14 @@ async function runAgentTurn(logger, opts) {
|
|
|
330
346
|
}
|
|
331
347
|
catch (err) {
|
|
332
348
|
const message = err.message;
|
|
333
|
-
logger.error(
|
|
349
|
+
logger.error('Shell execution failed', { error: message });
|
|
334
350
|
await opts.onBlock({
|
|
335
|
-
kind:
|
|
351
|
+
kind: 'terminalOutput',
|
|
336
352
|
text: `[terminal error]\n${message}`,
|
|
337
353
|
});
|
|
338
354
|
send({
|
|
339
355
|
session_id: sessionId,
|
|
340
|
-
sender:
|
|
356
|
+
sender: 'client',
|
|
341
357
|
content: `Failed to execute shell script: ${message}`,
|
|
342
358
|
is_terminal_output: true,
|
|
343
359
|
is_error: true,
|
|
@@ -348,16 +364,16 @@ async function runAgentTurn(logger, opts) {
|
|
|
348
364
|
}
|
|
349
365
|
const reasoning = cleanReasoning(content);
|
|
350
366
|
if (reasoning) {
|
|
351
|
-
await opts.onBlock({ kind:
|
|
367
|
+
await opts.onBlock({ kind: 'reasoning', text: reasoning });
|
|
352
368
|
}
|
|
353
369
|
});
|
|
354
|
-
ws.on(
|
|
355
|
-
logger.error(
|
|
370
|
+
ws.on('error', (err) => {
|
|
371
|
+
logger.error('Agent WebSocket error', { error: err.message });
|
|
356
372
|
finish(err);
|
|
357
373
|
});
|
|
358
|
-
ws.on(
|
|
374
|
+
ws.on('close', () => {
|
|
359
375
|
if (!settled)
|
|
360
|
-
finish(new Error(
|
|
376
|
+
finish(new Error('Agent WebSocket closed before final answer'));
|
|
361
377
|
});
|
|
362
378
|
});
|
|
363
379
|
}
|
|
@@ -369,10 +385,10 @@ function extractFinalAnswerFromHistory(historyJson) {
|
|
|
369
385
|
const history = JSON.parse(historyJson);
|
|
370
386
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
371
387
|
const entry = history[i];
|
|
372
|
-
if (entry.role !==
|
|
388
|
+
if (entry.role !== 'assistant')
|
|
373
389
|
continue;
|
|
374
|
-
const content = typeof entry.content ===
|
|
375
|
-
const fa = extractTagged(content,
|
|
390
|
+
const content = typeof entry.content === 'string' ? entry.content : '';
|
|
391
|
+
const fa = extractTagged(content, 'final_answer');
|
|
376
392
|
if (fa)
|
|
377
393
|
return fa;
|
|
378
394
|
}
|
|
@@ -9,23 +9,21 @@ exports.omnikeyWsUrl = omnikeyWsUrl;
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const os_1 = __importDefault(require("os"));
|
|
12
|
-
const DEFAULT_HOST =
|
|
12
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
13
13
|
const DEFAULT_PORT = 7071;
|
|
14
|
-
const DEFAULT_SQLITE = path_1.default.join(os_1.default.homedir(),
|
|
15
|
-
const CONFIG_PATH = path_1.default.join(os_1.default.homedir(),
|
|
14
|
+
const DEFAULT_SQLITE = path_1.default.join(os_1.default.homedir(), '.omnikey', 'omnikey-selfhosted.sqlite');
|
|
15
|
+
const CONFIG_PATH = path_1.default.join(os_1.default.homedir(), '.omnikey', 'config.json');
|
|
16
16
|
let cached = null;
|
|
17
17
|
function resolveSqlitePath(raw) {
|
|
18
|
-
if (typeof raw ===
|
|
19
|
-
return path_1.default.isAbsolute(raw)
|
|
20
|
-
? raw
|
|
21
|
-
: path_1.default.join(os_1.default.homedir(), ".omnikey", raw);
|
|
18
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
19
|
+
return path_1.default.isAbsolute(raw) ? raw : path_1.default.join(os_1.default.homedir(), '.omnikey', raw);
|
|
22
20
|
}
|
|
23
21
|
return DEFAULT_SQLITE;
|
|
24
22
|
}
|
|
25
23
|
function resolvePort(raw) {
|
|
26
|
-
if (typeof raw ===
|
|
24
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
27
25
|
return raw;
|
|
28
|
-
if (typeof raw ===
|
|
26
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
29
27
|
const n = Number(raw);
|
|
30
28
|
if (Number.isFinite(n))
|
|
31
29
|
return n;
|
|
@@ -38,7 +36,7 @@ function loadOmnikeyConfig() {
|
|
|
38
36
|
let parsed = {};
|
|
39
37
|
try {
|
|
40
38
|
if (fs_1.default.existsSync(CONFIG_PATH)) {
|
|
41
|
-
const raw = fs_1.default.readFileSync(CONFIG_PATH,
|
|
39
|
+
const raw = fs_1.default.readFileSync(CONFIG_PATH, 'utf-8');
|
|
42
40
|
parsed = JSON.parse(raw);
|
|
43
41
|
}
|
|
44
42
|
}
|
|
@@ -50,7 +48,7 @@ function loadOmnikeyConfig() {
|
|
|
50
48
|
cached = {
|
|
51
49
|
sqlitePath: resolveSqlitePath(parsed.SQLITE_PATH),
|
|
52
50
|
omnikeyPort: resolvePort(parsed.OMNIKEY_PORT),
|
|
53
|
-
omnikeyHost: typeof parsed.OMNIKEY_HOST ===
|
|
51
|
+
omnikeyHost: typeof parsed.OMNIKEY_HOST === 'string' && parsed.OMNIKEY_HOST.trim()
|
|
54
52
|
? parsed.OMNIKEY_HOST
|
|
55
53
|
: DEFAULT_HOST,
|
|
56
54
|
};
|
|
@@ -62,6 +60,6 @@ function omnikeyBaseUrl() {
|
|
|
62
60
|
}
|
|
63
61
|
function omnikeyWsUrl(path) {
|
|
64
62
|
const { omnikeyHost, omnikeyPort } = loadOmnikeyConfig();
|
|
65
|
-
const suffix = path.startsWith(
|
|
63
|
+
const suffix = path.startsWith('/') ? path : `/${path}`;
|
|
66
64
|
return `ws://${omnikeyHost}:${omnikeyPort}${suffix}`;
|
|
67
65
|
}
|