@exreve/exk 1.0.22 → 1.0.24

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.
@@ -427,11 +427,15 @@ export class AgentSessionManager {
427
427
  const mcpServer = this.buildMcpServer(sessionId);
428
428
  if (mcpServer) {
429
429
  const toolHint = getModuleToolHint(session.enabledModules || []);
430
+ console.log(`[agentSession] MCP server created: name=${mcpServer.name}, hasHint=${!!toolHint}`);
431
+ console.log(`[agentSession] MCP server keys:`, Object.keys(mcpServer));
432
+ console.log(`[agentSession] MCP server type:`, mcpServer.type);
430
433
  return {
431
- mcpServers: [mcpServer],
432
- ...(toolHint ? { systemPrompt: toolHint } : {})
434
+ mcpServers: { [mcpServer.name]: mcpServer },
435
+ ...(toolHint ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: toolHint } } : {})
433
436
  };
434
437
  }
438
+ console.log(`[agentSession] No MCP server created (enabledModules=${JSON.stringify(session.enabledModules)})`);
435
439
  return {};
436
440
  })(),
437
441
  ...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
package/dist/index.js CHANGED
@@ -202,8 +202,19 @@ function getCliVersion() {
202
202
  }
203
203
  const CURRENT_FILE = fileURLToPath(import.meta.url);
204
204
  const __dirname = path.dirname(CURRENT_FILE);
205
+ let cachedCliHash = null;
205
206
  function getCliHash() {
206
- return createHash('sha256').update(fsSync.readFileSync(CURRENT_FILE)).digest('hex');
207
+ if (cachedCliHash)
208
+ return cachedCliHash;
209
+ try {
210
+ const hash = createHash('sha256').update(fsSync.readFileSync(CURRENT_FILE)).digest('hex');
211
+ cachedCliHash = hash;
212
+ return hash;
213
+ }
214
+ catch {
215
+ // File may be temporarily missing during update — return safe fallback
216
+ return 'updating';
217
+ }
207
218
  }
208
219
  async function checkForUpdate() {
209
220
  try {
@@ -221,48 +232,108 @@ async function checkForUpdate() {
221
232
  return null;
222
233
  }
223
234
  }
235
+ let isUpdating = false;
224
236
  async function replaceSelf(tarballBuffer) {
237
+ isUpdating = true;
225
238
  const extractDir = path.join(os.tmpdir(), `ttc-update-${Date.now()}`);
226
- await fs.mkdir(extractDir, { recursive: true });
227
- const tarPath = path.join(extractDir, 'ttc-cli.tar.gz');
228
- await fs.writeFile(tarPath, tarballBuffer);
229
- execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
230
- await fs.unlink(tarPath);
231
- // Preserve user config/token files (never overwrite on update)
232
- const preserveFiles = ['device-config.json', 'device-id.json', 'config.json'];
233
- const preserved = {};
234
- for (const f of preserveFiles) {
239
+ const stagingDir = path.join(os.tmpdir(), `ttc-stage-${Date.now()}`);
240
+ try {
241
+ await fs.mkdir(extractDir, { recursive: true });
242
+ const tarPath = path.join(extractDir, 'ttc-cli.tar.gz');
243
+ await fs.writeFile(tarPath, tarballBuffer);
244
+ execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
245
+ await fs.unlink(tarPath);
246
+ // Preserve user config/token files (never overwrite on update)
247
+ const preserveFiles = ['device-config.json', 'device-id.json', 'config.json'];
248
+ const preserved = {};
249
+ for (const f of preserveFiles) {
250
+ try {
251
+ preserved[f] = await fs.readFile(path.join(CONFIG_DIR, f));
252
+ }
253
+ catch {
254
+ /* file may not exist */
255
+ }
256
+ }
257
+ // Build the complete new state in a staging directory first.
258
+ // This ensures we never delete the running app until the replacement is
259
+ // fully prepared and verified — no window where files are missing.
260
+ const cliStaging = path.join(stagingDir, 'cli');
261
+ const sharedStaging = path.join(stagingDir, 'shared');
262
+ await fs.cp(path.join(extractDir, 'cli'), cliStaging, { recursive: true });
263
+ await fs.cp(path.join(extractDir, 'shared'), sharedStaging, { recursive: true });
264
+ await fs.copyFile(path.join(extractDir, 'package.json'), path.join(stagingDir, 'package.json'));
265
+ // Restore preserved config into staging
266
+ for (const f of preserveFiles) {
267
+ if (preserved[f])
268
+ await fs.writeFile(path.join(stagingDir, f), preserved[f]);
269
+ }
270
+ // Sanity check: staging must contain a valid dist/index.js
271
+ const indexJs = path.join(cliStaging, 'dist', 'index.js');
272
+ try {
273
+ const stat = await fs.stat(indexJs);
274
+ if (stat.size === 0)
275
+ throw new Error('Empty index.js');
276
+ }
277
+ catch (err) {
278
+ throw new Error(`Staged update is invalid (missing or empty dist/index.js): ${err.message}`);
279
+ }
280
+ // Atomic swap: rename staging dirs to final locations.
281
+ // rename() is atomic on the same filesystem. We move the old dirs out of
282
+ // the way first, then rename new ones in. If anything fails mid-swap we
283
+ // can attempt to roll back.
284
+ const cliDest = path.join(CONFIG_DIR, 'cli');
285
+ const sharedDest = path.join(CONFIG_DIR, 'shared');
286
+ const cliOld = cliDest + '.old';
287
+ const sharedOld = sharedDest + '.old';
288
+ // Move current dirs to .old (must exist for this to work)
289
+ await fs.rm(cliOld, { recursive: true, force: true });
290
+ await fs.rm(sharedOld, { recursive: true, force: true });
291
+ if (fsSync.existsSync(cliDest)) {
292
+ await fs.rename(cliDest, cliOld);
293
+ }
294
+ if (fsSync.existsSync(sharedDest)) {
295
+ await fs.rename(sharedDest, sharedOld);
296
+ }
297
+ // Move staged dirs into place
235
298
  try {
236
- preserved[f] = await fs.readFile(path.join(CONFIG_DIR, f));
299
+ await fs.rename(cliStaging, cliDest);
300
+ await fs.rename(sharedStaging, sharedDest);
301
+ await fs.copyFile(path.join(stagingDir, 'package.json'), path.join(CONFIG_DIR, 'package.json'));
302
+ }
303
+ catch (swapErr) {
304
+ // Rollback: restore old dirs
305
+ console.error(`⚠ Swap failed: ${swapErr.message}, rolling back...`);
306
+ try {
307
+ if (fsSync.existsSync(cliOld))
308
+ await fs.rename(cliOld, cliDest);
309
+ if (fsSync.existsSync(sharedOld))
310
+ await fs.rename(sharedOld, sharedDest);
311
+ }
312
+ catch (rollbackErr) {
313
+ console.error(`⚠ Rollback also failed: ${rollbackErr.message}`);
314
+ }
315
+ throw swapErr;
316
+ }
317
+ // Clean up old dirs and temp dirs
318
+ await fs.rm(cliOld, { recursive: true, force: true }).catch(() => { });
319
+ await fs.rm(sharedOld, { recursive: true, force: true }).catch(() => { });
320
+ await fs.rm(extractDir, { recursive: true, force: true });
321
+ await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => { });
322
+ // Invalidate cached hash since we just replaced the binary
323
+ cachedCliHash = null;
324
+ console.log('✓ CLI updated');
325
+ const npmPaths = [path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'), path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')];
326
+ const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
327
+ try {
328
+ execSync(`"${npmBin}" install --omit=dev`, { cwd: CONFIG_DIR, stdio: 'inherit' });
329
+ console.log('✓ Dependencies updated');
237
330
  }
238
331
  catch {
239
- /* file may not exist */
332
+ console.warn('⚠ npm install failed');
240
333
  }
241
334
  }
242
- // Replace cli/ and shared/ in CONFIG_DIR
243
- const cliDest = path.join(CONFIG_DIR, 'cli');
244
- const sharedDest = path.join(CONFIG_DIR, 'shared');
245
- await fs.rm(cliDest, { recursive: true, force: true });
246
- await fs.rm(sharedDest, { recursive: true, force: true });
247
- await fs.cp(path.join(extractDir, 'cli'), cliDest, { recursive: true });
248
- await fs.cp(path.join(extractDir, 'shared'), sharedDest, { recursive: true });
249
- // Update package.json and npm install
250
- await fs.copyFile(path.join(extractDir, 'package.json'), path.join(CONFIG_DIR, 'package.json'));
251
- await fs.rm(extractDir, { recursive: true, force: true });
252
- // Restore preserved config
253
- for (const f of preserveFiles) {
254
- if (preserved[f])
255
- await fs.writeFile(path.join(CONFIG_DIR, f), preserved[f]);
256
- }
257
- console.log('✓ CLI updated');
258
- const npmPaths = [path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'), path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')];
259
- const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
260
- try {
261
- execSync(`"${npmBin}" install --omit=dev`, { cwd: CONFIG_DIR, stdio: 'inherit' });
262
- console.log('✓ Dependencies updated');
263
- }
264
- catch {
265
- console.warn('⚠ npm install failed');
335
+ finally {
336
+ isUpdating = false;
266
337
  }
267
338
  }
268
339
  async function selfUpdate(force = false) {
@@ -1716,6 +1787,10 @@ async function runDaemon(foreground = false, email) {
1716
1787
  // ========== Version & Update Handlers ==========
1717
1788
  // Respond with CLI version info
1718
1789
  socket.on('version:info', (_data, callback) => {
1790
+ if (isUpdating) {
1791
+ callback({ success: false, error: 'Update in progress' });
1792
+ return;
1793
+ }
1719
1794
  callback({
1720
1795
  success: true,
1721
1796
  version: getCliVersion(),
@@ -1728,6 +1803,10 @@ async function runDaemon(foreground = false, email) {
1728
1803
  });
1729
1804
  // Force update: npm update -g @exreve/exk then restart PM2
1730
1805
  socket.on('force-update', (_data, callback) => {
1806
+ if (isUpdating) {
1807
+ callback?.({ success: false, error: 'Update already in progress' });
1808
+ return;
1809
+ }
1731
1810
  if (foreground) {
1732
1811
  console.log('[force-update] Received force update command from server');
1733
1812
  }
@@ -1770,6 +1849,10 @@ async function runDaemon(foreground = false, email) {
1770
1849
  });
1771
1850
  // ========== update:start handler (legacy compatibility) ==========
1772
1851
  socket.on('update:start', (_data, callback) => {
1852
+ if (isUpdating) {
1853
+ callback?.({ success: false, error: 'Update already in progress' });
1854
+ return;
1855
+ }
1773
1856
  // Use npm-based self-update
1774
1857
  if (foreground) {
1775
1858
  console.log('[update:start] Starting npm self-update...');
@@ -15,7 +15,7 @@ function createUserChoiceTool(onChoiceRequest) {
15
15
  options: z.array(z.object({ label: z.string(), value: z.string() })),
16
16
  timeout: z.number().optional()
17
17
  };
18
- return tool('user_choice_request', 'Request user input when making decisions. Use this when you need the user to choose between options or provide input on a decision. This tool will present a modal to the user with your question and wait for their response.', schema, async (args, _extra) => {
18
+ return tool('user_choice_request', 'Present a choice modal to the user and wait for their selection. Use this whenever you need a decision, confirmation, or preference from the user. The AskUserQuestion tool is disabled this is your only way to get interactive user input. Always use this before proceeding with ambiguous tasks or destructive actions.', schema, async (args, _extra) => {
19
19
  if (!onChoiceRequest) {
20
20
  return {
21
21
  content: [
@@ -101,10 +101,15 @@ export function getModuleToolHint(enabledModules) {
101
101
  return null;
102
102
  const toolDescriptions = [];
103
103
  if (enabledModules.includes('user-choice')) {
104
- toolDescriptions.push('- user_choice_request: Request user input when making decisions. Presents a modal to the user with options and waits for their response. Use this when you need the user to choose between options or confirm an action.');
104
+ toolDescriptions.push(`- user_choice_request(question: str, options: [{label: str, value: str}], timeout?: int): Present a modal to the remote user with a question and clickable options. Returns the user's selection.
105
+ IMPORTANT: The built-in AskUserQuestion tool is DISABLED. Whenever you need user input, decisions, or confirmations, you MUST use user_choice_request instead.
106
+ Always call this tool when:
107
+ - There are multiple valid approaches and you need the user to pick one
108
+ - You need explicit confirmation before a destructive or significant action
109
+ - The user's preference matters for how to proceed`);
105
110
  }
106
111
  // Add more module tool descriptions here as they are implemented
107
112
  if (toolDescriptions.length === 0)
108
113
  return null;
109
- return `You have access to the following custom MCP tools:\n${toolDescriptions.join('\n')}\n\nUse these tools proactively when they are relevant to the task.`;
114
+ return `You have access to the following custom MCP tools:\n${toolDescriptions.join('\n')}\n\nThese tools are your ONLY way to interact with the user (AskUserQuestion is disabled). Call them directly — do NOT ask the user to type responses in chat.`;
110
115
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {