omnikey-cli 1.0.15 → 1.0.16

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.
@@ -42,11 +42,17 @@ When you generate shell scripts, make them clear, efficient, and focused on gath
42
42
  - If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
43
43
 
44
44
  **Web tools:**
45
- You have access to web tools you can call at any time during a turn:
46
- - \`web_fetch(url)\`: Fetches the text content of any publicly accessible URL. Use it to retrieve documentation, error references, API guides, release notes, or any other web resource that would help answer the user's question.
47
- - \`web_search(query)\`: Searches the web and returns a list of relevant results (title, URL, snippet). Use it when you need to discover the right URL before fetching, or when a quick summary of search results is sufficient.
45
+ You have access to web tools, but you must use them sparingly and only when explicitly required:
46
+ - \`web_fetch(url)\`: Only call this when the user has provided a specific URL in their current input or stored instructions and you need to retrieve its contents.
47
+ - \`web_search(query)\`: Only call this when the user has explicitly asked you to search the web or look something up online.
48
48
 
49
- Use these tools proactively whenever the question involves current information, external documentation, or anything not already available in the conversation or machine output. You may call web tools multiple times in a single turn; call \`web_fetch\` on a promising URL from \`web_search\` results to get full details. Web tool results are injected back into the conversation automatically; continue reasoning and then emit your shell script or final answer as normal.
49
+ Do NOT use web tools proactively. Do NOT call them to look up documentation, error references, or general information you could infer from the machine output or your own knowledge. Your primary workflow is to generate shell scripts, wait for the terminal output, and reason from that output. Only reach for web tools when there is a clear, explicit instruction or a URL provided by the user.
50
+
51
+ **User message tags:**
52
+ User messages may be prefixed with special tags that indicate their origin:
53
+ - \`TERMINAL OUTPUT:\` — the content is stdout/stderr returned from a previously requested \`<shell_script>\`. Parse it as machine output and use it to continue your reasoning toward a \`<final_answer>\` or a follow-up \`<shell_script>\`.
54
+ - \`COMMAND ERROR:\` — the shell script failed or the terminal returned a non-zero exit code. Treat the content as error output: diagnose the failure, then either emit a corrected \`<shell_script>\` or explain the issue in a \`<final_answer>\`.
55
+ - No prefix — the content is a direct message from the user; treat it as the primary request or question to address.
50
56
 
51
57
  **Interaction rules:**
52
58
  - When you need to execute ANY shell command, respond with a single \`<shell_script>\` block that contains the FULL script to run.
@@ -255,8 +255,13 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
255
255
  let toolIterations = 0;
256
256
  while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
257
257
  toolIterations++;
258
- session.history.push(result.assistantMessage);
259
258
  const toolCalls = result.tool_calls ?? [];
259
+ // If the model claims tool_calls but sent none, treat it as a normal text
260
+ // response — pushing an assistant message with no following tool results
261
+ // would leave the history ending with an assistant turn, causing a 400.
262
+ if (!toolCalls.length)
263
+ break;
264
+ session.history.push(result.assistantMessage);
260
265
  log.info('Agent executing tool calls', {
261
266
  sessionId,
262
267
  turn: session.turns,
@@ -265,6 +270,18 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
265
270
  });
266
271
  const toolResults = await Promise.all(toolCalls.map(async (tc) => {
267
272
  const args = tc.arguments;
273
+ // Notify the frontend that a web tool call is about to execute.
274
+ const webCallContent = tc.name === 'web_search'
275
+ ? `Searching the web for: "${args.query ?? ''}"`
276
+ : `Fetching URL: ${args.url ?? ''}`;
277
+ send({
278
+ session_id: sessionId,
279
+ sender: 'agent',
280
+ content: webCallContent,
281
+ is_terminal_output: false,
282
+ is_error: false,
283
+ is_web_call: true,
284
+ });
268
285
  const toolResult = await (0, web_search_provider_1.executeTool)(tc.name, args, log);
269
286
  log.info('Tool call completed', {
270
287
  sessionId,
@@ -287,10 +304,14 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
287
304
  });
288
305
  await recordUsage(result);
289
306
  }
307
+ log.info('Finished reasoning and tool calls: ', {
308
+ reason: result.finish_reason,
309
+ });
310
+ const content = result.content.trim();
290
311
  // If the tool loop was exhausted while the model still wants more tool calls,
291
312
  // the last result has empty content. Force one final no-tools call so the model
292
313
  // must synthesize a text answer from everything gathered so far.
293
- if (result.finish_reason === 'tool_calls') {
314
+ if (result.finish_reason === 'tool_calls' || !content) {
294
315
  log.warn('Tool iteration limit reached with pending tool calls; forcing final text response', {
295
316
  sessionId,
296
317
  turn: session.turns,
@@ -309,7 +330,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
309
330
  });
310
331
  await recordUsage(result);
311
332
  }
312
- const content = result.content.trim();
313
333
  if (!content) {
314
334
  log.warn('Agent LLM returned empty content; sending generic error to client.');
315
335
  const errorMessage = 'The agent returned an empty response. Please try again.';
@@ -19,19 +19,31 @@ const app = (0, express_1.default)();
19
19
  const PORT = Number(config_1.config.port);
20
20
  app.use((0, cors_1.default)());
21
21
  app.use(express_1.default.json());
22
+ // Landing page
23
+ app.use(express_1.default.static(path_1.default.join(process.cwd(), 'public')));
22
24
  app.use('/api/subscription', (0, subscriptionRoutes_1.createSubscriptionRouter)(logger_1.logger));
23
25
  app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
24
26
  app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
25
- app.get('/macos/download', (req, res) => {
27
+ app.get('/macos/download', (_req, res) => {
26
28
  const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
27
- res.download(dmgPath, 'OmniKeyAI.dmg', (err) => {
28
- if (err) {
29
- logger_1.logger.error('Failed to send OmniKeyAI.dmg for download.', { error: err });
30
- if (!res.headersSent) {
31
- res.status(500).send('Unable to download file.');
32
- }
29
+ if (!fs_1.default.existsSync(dmgPath)) {
30
+ res.status(404).send('File not found.');
31
+ return;
32
+ }
33
+ res.set({
34
+ 'Content-Type': 'application/octet-stream',
35
+ 'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
36
+ 'Content-Encoding': 'gzip',
37
+ });
38
+ const fileStream = fs_1.default.createReadStream(dmgPath);
39
+ const gzip = zlib_1.default.createGzip();
40
+ fileStream.on('error', (err) => {
41
+ logger_1.logger.error('Failed to send OmniKeyAI.dmg for download.', { error: err });
42
+ if (!res.headersSent) {
43
+ res.status(500).send('Unable to download file.');
33
44
  }
34
45
  });
46
+ fileStream.pipe(gzip).pipe(res);
35
47
  });
36
48
  // Sparkle appcast feed for macOS updates.
37
49
  // This feed uses the existing /macos/download endpoint as the
@@ -52,8 +64,8 @@ app.get('/macos/appcast', (req, res) => {
52
64
  const appcastUrl = `${baseUrl}/macos/appcast`;
53
65
  // These should match the values embedded into the macOS app
54
66
  // Info.plist in macOS/build_release_dmg.sh.
55
- const bundleVersion = '13';
56
- const shortVersion = '1.0.12';
67
+ const bundleVersion = '14';
68
+ const shortVersion = '1.0.13';
57
69
  const xml = `<?xml version="1.0" encoding="utf-8"?>
58
70
  <rss version="2.0"
59
71
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
@@ -81,7 +93,7 @@ app.get('/macos/appcast', (req, res) => {
81
93
  // ── Windows distribution endpoints ───────────────────────────────────────────
82
94
  // These should match the values in windows/OmniKey.Windows.csproj
83
95
  // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
84
- const WIN_VERSION = '1.1';
96
+ const WIN_VERSION = '1.2';
85
97
  const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
86
98
  const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
87
99
  // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
@@ -128,6 +140,9 @@ app.get('/windows/update', (req, res) => {
128
140
  app.get('/health', (_req, res) => {
129
141
  res.json({ status: 'ok' });
130
142
  });
143
+ app.get('*', (_req, res) => {
144
+ res.sendFile(path_1.default.join(process.cwd(), 'public', 'index.html'));
145
+ });
131
146
  let server = null;
132
147
  async function start() {
133
148
  try {
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.15",
7
+ "version": "1.0.16",
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",