omnikey-cli 1.1.0 → 1.2.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.
@@ -603,19 +603,19 @@ async function runAgentTurnInternal(sessionId, subscription, clientMessage, send
603
603
  // is not the final turn (e.g. plain-text conclusion after terminal
604
604
  // output). Treat it as a final answer so the client is never left
605
605
  // hanging.
606
- log.info('Agent returned untagged content on a non-final turn; treating as final answer', {
606
+ log.info('Agent returned untagged content on a non-final turn; treating as assistant response and looping the function again.', {
607
607
  sessionId,
608
608
  subscriptionId: subscription.id,
609
609
  turn: session.turns,
610
610
  });
611
611
  (0, utils_1.pushToSessionHistory)(log, session, { role: 'assistant', content });
612
612
  await persistSessionToDB(sessionId, session);
613
- send({
614
- session_id: sessionId,
613
+ await runAgentTurnInternal(sessionId, subscription, {
615
614
  sender: 'agent',
616
- content: `<final_answer>\n${content}\n</final_answer>`,
617
- });
618
- void (0, sessionGrouping_1.updateSessionGroup)(sessionId, subscription.id);
615
+ session_id: sessionId,
616
+ content: '',
617
+ is_web_call: true,
618
+ }, send, logger_1.logger, options);
619
619
  }
620
620
  else {
621
621
  log.warn('Agent returned empty content with no recognized tags; sending error', {
@@ -797,7 +797,12 @@ function buildTranscript(raw) {
797
797
  break;
798
798
  }
799
799
  }
800
- currentAssistant.text = finalText || blocks.map((b) => b.text).join('\n\n').trim();
800
+ currentAssistant.text =
801
+ finalText ||
802
+ blocks
803
+ .map((b) => b.text)
804
+ .join('\n\n')
805
+ .trim();
801
806
  messages.push(currentAssistant);
802
807
  currentAssistant = null;
803
808
  };
@@ -44,6 +44,68 @@ async function runSQLiteMigrations(logger) {
44
44
  logger.info(`SQLite migration: added column ${table}.${column}`);
45
45
  }
46
46
  }
47
+ // mcp_servers was originally created with UNIQUE on both subscription_id and
48
+ // name as column-level constraints (SQLite auto-indexes). These can't be
49
+ // dropped with DROP INDEX — the only fix is to recreate the table with the
50
+ // correct schema (composite unique on subscription_id+name only).
51
+ await migrateMcpServersTableIfNeeded(logger);
52
+ }
53
+ async function migrateMcpServersTableIfNeeded(logger) {
54
+ // Check if the old schema is still in place by inspecting the CREATE TABLE sql.
55
+ const rows = (await sequelize.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name='mcp_servers'`))[0];
56
+ if (!rows.length)
57
+ return; // table doesn't exist yet — sync() will create it correctly
58
+ const createSql = rows[0].sql;
59
+ // Old schema has UNIQUE on subscription_id at the column level.
60
+ // New schema only has the composite index mcp_servers_subscription_id_name.
61
+ const needsMigration = /`subscription_id`[^,]*UNIQUE/i.test(createSql);
62
+ if (!needsMigration)
63
+ return;
64
+ logger.info('SQLite migration: recreating mcp_servers table to remove stale UNIQUE constraints');
65
+ await sequelize.query('PRAGMA foreign_keys = OFF');
66
+ try {
67
+ await sequelize.query('BEGIN TRANSACTION');
68
+ await sequelize.query(`
69
+ CREATE TABLE \`mcp_servers_new\` (
70
+ \`id\` VARCHAR(255) NOT NULL PRIMARY KEY,
71
+ \`subscription_id\` VARCHAR(255) NOT NULL REFERENCES \`subscriptions\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE,
72
+ \`name\` VARCHAR(100) NOT NULL,
73
+ \`description\` VARCHAR(500),
74
+ \`transport\` VARCHAR(16) NOT NULL DEFAULT 'stdio',
75
+ \`command\` VARCHAR(500),
76
+ \`args\` JSON NOT NULL DEFAULT '[]',
77
+ \`env\` JSON NOT NULL DEFAULT '{}',
78
+ \`url\` VARCHAR(1000),
79
+ \`headers\` JSON NOT NULL DEFAULT '{}',
80
+ \`is_enabled\` TINYINT(1) NOT NULL DEFAULT 1,
81
+ \`last_connected_at\` DATETIME,
82
+ \`last_error\` TEXT,
83
+ \`createdAt\` DATETIME NOT NULL,
84
+ \`updatedAt\` DATETIME NOT NULL
85
+ )
86
+ `);
87
+ await sequelize.query(`
88
+ INSERT INTO \`mcp_servers_new\`
89
+ SELECT id, subscription_id, name, description, transport, command, args, env,
90
+ url, headers, is_enabled, last_connected_at, last_error, createdAt, updatedAt
91
+ FROM \`mcp_servers\`
92
+ `);
93
+ await sequelize.query('DROP TABLE `mcp_servers`');
94
+ await sequelize.query('ALTER TABLE `mcp_servers_new` RENAME TO `mcp_servers`');
95
+ await sequelize.query(`
96
+ CREATE UNIQUE INDEX IF NOT EXISTS \`mcp_servers_subscription_id_name\`
97
+ ON \`mcp_servers\` (\`subscription_id\`, \`name\`)
98
+ `);
99
+ await sequelize.query('COMMIT');
100
+ logger.info('SQLite migration: mcp_servers table recreated successfully');
101
+ }
102
+ catch (err) {
103
+ await sequelize.query('ROLLBACK');
104
+ throw err;
105
+ }
106
+ finally {
107
+ await sequelize.query('PRAGMA foreign_keys = ON');
108
+ }
47
109
  }
48
110
  async function initDatabase(logger) {
49
111
  try {
@@ -107,7 +107,7 @@ app.get('/macos/appcast', (req, res) => {
107
107
  // ── Windows distribution endpoints ───────────────────────────────────────────
108
108
  // These should match the values in windows/OmniKey.Windows.csproj
109
109
  // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
110
- const WIN_VERSION = '1.12';
110
+ const WIN_VERSION = '1.13';
111
111
  const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
112
112
  const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
113
113
  // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
@@ -152,15 +152,19 @@ app.get('/windows/update', (req, res) => {
152
152
  releaseNotes: [
153
153
  `What's new in ${WIN_VERSION}`,
154
154
  ``,
155
- `• Brand-new WPF shell Agent Chat, OmniAgent Session, Task Instructions, Scheduled Jobs, Job Run History, MCP Servers, Manual, Subscription, and Check Updates all reachable from a unified sidebar.`,
156
- `• Agent Chat with session sidebar, search, hover-to-delete, context-window indicator, default task-instruction picker (with a built-in "No task instructions" option).`,
157
- `• OmniAgent Session replaces the old thinking window sticky session picker, streaming collapsible timeline (web search, MCP, reasoning, terminal) and a copyable final-answer card. Selecting an existing session previews its last turn in-place.`,
158
- `• Scheduled Jobs redesigned: clearer labels, self-contained Active toggle, restructured Save / Run now / Reset / Delete action bar, tooltips throughout.`,
159
- `• Theme reworked to match the app icon cyan + light purple. The Windows system-accent leak (orange/red buttons) is gone everywhere.`,
160
- `• Modern typography (Aptos / Inter / Segoe UI Variable Display).`,
161
- `• Mouse-wheel scroll fixed across Chat and OmniAgent pages.`,
162
- `• Production-default backend URL: shipping binary defaults to https://omnikeyai.ca.`,
163
- `• Internals: removed ~5,200 lines of unwired WinForms code; build is warning-free.`,
155
+ `• Projects: chats are now grouped by project in the sidebar collapsible "folder" headers per group, a session count badge, and per-header collapse state that survives streaming turns.`,
156
+ `• Projects: new project picker in the composer toolbar (next to the task-instruction selector) pick the project for your next turn, mirrors the macOS "Select project" menu. Auto-hides until the backend has classified at least one group.`,
157
+ `• Projects: the chosen project is stamped onto the outbound message and the optimistic session placeholder, so new chats appear under the right header immediately.`,
158
+ `• Chat: messages now sit in a centered 820 DIP reading column on large monitors and stretch edge-to-edge on smaller windows matches the macOS layout exactly. User bubbles stay pinned right, assistant content stays pinned left, on every viewport.`,
159
+ `• Chat: new Final Answer card with a soft "paper" surface (mirrors macOS), copy button anchored bottom-right so it no longer overlaps long markdown headings, and an "Answer" tooltip on copy.`,
160
+ `• Chat: animated typing indicator (pulsing sparkle + three staggered dots) appears the moment you send your first message — matches macOS TypingDotsView.`,
161
+ `• Chat: extra breathing room between thinking-timeline steps so the agent's intermediate reasoning reads as discrete actions instead of a cramped wall.`,
162
+ `• Markdown: brand-new Nord-themed renderer no more white-background leaks from the underlying MdXaml engine on paragraphs, blockquotes, lists, tables, or inline code.`,
163
+ `• Markdown: bullets and numbered lists are no longer clipped on the left edge.`,
164
+ `• Markdown: inline code now renders as a soft pill (BadgeFill) instead of a dark slab; fenced code blocks keep their rounded macOS-style chrome with language label + copy.`,
165
+ `• MCP Servers: editor now supports custom HTTP headers — one Key: Value per line, monospace input, persisted alongside the URL. Authorization headers are unredacted on edit so they round-trip cleanly, and stale fetches won't clobber what you're typing.`,
166
+ `• Composer: capped + centered at 820 DIP on wide monitors for a balanced layout, full pane width on smaller windows.`,
167
+ `• Theme: shared interactive-surface brushes (Hover, Press, CodeBackground, UserBubble, AssistantText, DangerSoft, FinalAnswerSurface, BadgeFill) promoted to NordTheme.xaml so every page stays in visual lockstep.`,
164
168
  ].join('\n'),
165
169
  });
166
170
  });
@@ -122,7 +122,12 @@ function mcpServerRouter() {
122
122
  return res.status(400).json({ error: 'Invalid MCP server data.' });
123
123
  }
124
124
  if (err?.name === 'SequelizeUniqueConstraintError') {
125
- return res.status(409).json({ error: 'An MCP server with that name already exists.' });
125
+ const isNameConflict = err?.fields?.includes('name') || err?.errors?.some((e) => e.path === 'name');
126
+ return res.status(409).json({
127
+ error: isNameConflict
128
+ ? 'An MCP server with that name already exists.'
129
+ : 'Failed to create MCP server due to a conflict.',
130
+ });
126
131
  }
127
132
  res.status(500).json({ error: 'Failed to create MCP server.' });
128
133
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
7
- "version": "1.1.0",
7
+ "version": "1.2.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",