@fcannizzaro/exocommand 1.0.10 → 1.1.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/dist/server.js CHANGED
@@ -2,9 +2,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod/v4";
3
3
  import { loadCommands } from "./config.js";
4
4
  import { executeCommand } from "./executor.js";
5
- const CONFIG_PATH = process.env.EXO_COMMAND_FILE || "./.exocommand";
6
5
  export function createServer(logger, options) {
7
- const taskMode = options?.taskMode ?? false;
6
+ const configPath = options.configPath;
7
+ const taskMode = options.taskMode ?? false;
8
+ const tui = options.tui ?? null;
9
+ const agentId = options.agentId ?? 0;
10
+ const projectKey = options.projectKey ?? "";
8
11
  const activeExecutions = new Map();
9
12
  // Build server options conditionally based on mode
10
13
  const serverOptions = taskMode
@@ -29,7 +32,7 @@ export function createServer(logger, options) {
29
32
  description: "List all available commands defined in the .exocommand config file",
30
33
  }, async () => {
31
34
  try {
32
- const commands = await loadCommands(CONFIG_PATH);
35
+ const commands = await loadCommands(configPath);
33
36
  logger.info("listCommands", `found ${commands.length} command(s)`);
34
37
  return {
35
38
  content: [
@@ -58,10 +61,10 @@ export function createServer(logger, options) {
58
61
  }
59
62
  });
60
63
  if (taskMode) {
61
- registerTaskMode(server, logger, activeExecutions);
64
+ registerTaskMode(server, logger, activeExecutions, configPath, tui, agentId, projectKey);
62
65
  }
63
66
  else {
64
- registerSyncMode(server, logger, activeExecutions);
67
+ registerSyncMode(server, logger, activeExecutions, configPath, tui, agentId, projectKey);
65
68
  }
66
69
  return server;
67
70
  }
@@ -80,17 +83,17 @@ function buildTaskServerOptions(activeExecutions) {
80
83
  defaultTaskPollInterval: 1000,
81
84
  };
82
85
  }
83
- function registerTaskMode(server, logger, activeExecutions) {
86
+ function registerTaskMode(server, logger, activeExecutions, configPath, tui, agentId, projectKey) {
84
87
  // Lazy-loaded to avoid pulling task dependencies when not needed
85
88
  const { registerTaskExecute } = require("./task.js");
86
- registerTaskExecute(server, logger, activeExecutions);
89
+ registerTaskExecute(server, logger, activeExecutions, configPath, tui, agentId, projectKey);
87
90
  }
88
91
  function getTaskContext(activeExecutions) {
89
92
  const { createTaskContext } = require("./task.js");
90
93
  return createTaskContext(activeExecutions);
91
94
  }
92
95
  // -- Sync mode: streaming via extra.sendNotification() --------------------
93
- function registerSyncMode(server, logger, activeExecutions) {
96
+ function registerSyncMode(server, logger, activeExecutions, configPath, tui, agentId, projectKey) {
94
97
  server.registerTool("execute", {
95
98
  title: "Execute Command",
96
99
  description: "Execute a predefined command by name. Streams stdout and stderr line-by-line as logging notifications on the response stream.",
@@ -103,12 +106,15 @@ function registerSyncMode(server, logger, activeExecutions) {
103
106
  .describe("Maximum execution time in seconds. If exceeded, the command is killed and buffered output is returned."),
104
107
  },
105
108
  }, async ({ name: commandName, timeout }, extra) => {
109
+ const executionId = crypto.randomUUID();
106
110
  logger.warn("execute", `running "${commandName}"`);
111
+ tui?.addExecution(executionId, commandName, agentId, projectKey);
107
112
  let commands;
108
113
  try {
109
- commands = await loadCommands(CONFIG_PATH);
114
+ commands = await loadCommands(configPath);
110
115
  }
111
116
  catch (err) {
117
+ tui?.updateExecution(executionId, "error");
112
118
  return {
113
119
  content: [{
114
120
  type: "text",
@@ -120,6 +126,7 @@ function registerSyncMode(server, logger, activeExecutions) {
120
126
  const cmd = commands.find((c) => c.name === commandName);
121
127
  if (!cmd) {
122
128
  const available = commands.map((c) => c.name).join(", ");
129
+ tui?.updateExecution(executionId, "error");
123
130
  return {
124
131
  content: [{
125
132
  type: "text",
@@ -128,7 +135,6 @@ function registerSyncMode(server, logger, activeExecutions) {
128
135
  isError: true,
129
136
  };
130
137
  }
131
- const executionId = crypto.randomUUID();
132
138
  const ac = new AbortController();
133
139
  activeExecutions.set(executionId, ac);
134
140
  // Propagate request cancellation
@@ -165,6 +171,7 @@ function registerSyncMode(server, logger, activeExecutions) {
165
171
  const status = timedOut
166
172
  ? `Command "${commandName}" timed out after ${timeout}s.`
167
173
  : `Command "${commandName}" was cancelled.`;
174
+ tui?.updateExecution(executionId, timedOut ? "timeout" : "cancelled");
168
175
  logger.warn("execute", `"${commandName}" ${timedOut ? "timed out" : "was cancelled"}`);
169
176
  return {
170
177
  content: [{
@@ -176,6 +183,7 @@ function registerSyncMode(server, logger, activeExecutions) {
176
183
  }
177
184
  if (result.exitCode !== 0) {
178
185
  const status = `Command "${commandName}" exited with code ${result.exitCode}`;
186
+ tui?.updateExecution(executionId, "error");
179
187
  logger.error("execute", `"${commandName}" exited with code ${result.exitCode}`);
180
188
  return {
181
189
  content: [{
@@ -185,6 +193,7 @@ function registerSyncMode(server, logger, activeExecutions) {
185
193
  isError: true,
186
194
  };
187
195
  }
196
+ tui?.updateExecution(executionId, "success");
188
197
  logger.success("execute", `"${commandName}" completed (exit code 0)`);
189
198
  return {
190
199
  content: [{
@@ -198,6 +207,7 @@ function registerSyncMode(server, logger, activeExecutions) {
198
207
  catch (err) {
199
208
  const output = lines.join("\n");
200
209
  const status = `Command "${commandName}" failed: ${err.message}`;
210
+ tui?.updateExecution(executionId, "error");
201
211
  logger.error("execute", `"${commandName}" failed: ${err.message}`);
202
212
  return {
203
213
  content: [{
package/dist/task.js CHANGED
@@ -3,7 +3,6 @@ import { InMemoryTaskStore, InMemoryTaskMessageQueue, } from "@modelcontextproto
3
3
  import { z } from "zod/v4";
4
4
  import { loadCommands } from "./config.js";
5
5
  import { executeCommand } from "./executor.js";
6
- const CONFIG_PATH = process.env.EXO_COMMAND_FILE || "./.exocommand";
7
6
  // Subclass InMemoryTaskStore to abort running processes on task cancellation.
8
7
  // The SDK calls updateTaskStatus("cancelled") when a tasks/cancel request arrives.
9
8
  class ExoTaskStore extends InMemoryTaskStore {
@@ -24,7 +23,7 @@ class ExoTaskStore extends InMemoryTaskStore {
24
23
  }
25
24
  }
26
25
  async function runBackgroundExecution(params) {
27
- const { taskId, commandName, command, cwd, signal, timeoutSignal, timeout, taskStore, server, logger, activeExecutions, } = params;
26
+ const { taskId, commandName, command, cwd, signal, timeoutSignal, timeout, taskStore, server, logger, activeExecutions, tui, } = params;
28
27
  const lines = [];
29
28
  let lineCount = 0;
30
29
  const log = async (level, loggerName, data) => {
@@ -56,6 +55,7 @@ async function runBackgroundExecution(params) {
56
55
  // task may have been cleaned up
57
56
  }
58
57
  if (task?.status === "cancelled") {
58
+ tui?.updateExecution(taskId, "cancelled");
59
59
  logger.warn("execute", `"${commandName}" was cancelled`);
60
60
  return;
61
61
  }
@@ -63,6 +63,7 @@ async function runBackgroundExecution(params) {
63
63
  const status = timedOut
64
64
  ? `Command "${commandName}" timed out after ${timeout}s.`
65
65
  : `Command "${commandName}" was cancelled.`;
66
+ tui?.updateExecution(taskId, timedOut ? "timeout" : "cancelled");
66
67
  logger.warn("execute", `"${commandName}" ${timedOut ? "timed out" : "was cancelled"}`);
67
68
  await taskStore.storeTaskResult(taskId, "failed", {
68
69
  content: [{
@@ -75,6 +76,7 @@ async function runBackgroundExecution(params) {
75
76
  }
76
77
  if (result.exitCode !== 0) {
77
78
  const status = `Command "${commandName}" exited with code ${result.exitCode}`;
79
+ tui?.updateExecution(taskId, "error");
78
80
  logger.error("execute", `"${commandName}" exited with code ${result.exitCode}`);
79
81
  await taskStore.storeTaskResult(taskId, "failed", {
80
82
  content: [{
@@ -85,6 +87,7 @@ async function runBackgroundExecution(params) {
85
87
  });
86
88
  return;
87
89
  }
90
+ tui?.updateExecution(taskId, "success");
88
91
  logger.success("execute", `"${commandName}" completed (exit code 0)`);
89
92
  await taskStore.storeTaskResult(taskId, "completed", {
90
93
  content: [{
@@ -98,6 +101,7 @@ async function runBackgroundExecution(params) {
98
101
  catch (err) {
99
102
  const output = lines.join("\n");
100
103
  const status = `Command "${commandName}" failed: ${err.message}`;
104
+ tui?.updateExecution(taskId, "error");
101
105
  logger.error("execute", `"${commandName}" failed: ${err.message}`);
102
106
  await taskStore.storeTaskResult(taskId, "failed", {
103
107
  content: [{
@@ -124,7 +128,10 @@ export function createTaskContext(activeExecutions) {
124
128
  },
125
129
  };
126
130
  }
127
- export function registerTaskExecute(server, logger, activeExecutions) {
131
+ export function registerTaskExecute(server, logger, activeExecutions, configPath, tui, agentId, projectKey) {
132
+ const resolvedTui = tui ?? null;
133
+ const resolvedAgentId = agentId ?? 0;
134
+ const resolvedProjectKey = projectKey ?? "";
128
135
  server.experimental.tasks.registerToolTask("execute", {
129
136
  title: "Execute Command",
130
137
  description: "Execute a predefined command by name. Streams stdout and stderr via logging notifications and task status updates.",
@@ -143,7 +150,7 @@ export function registerTaskExecute(server, logger, activeExecutions) {
143
150
  // Validate before creating a task — throw to fail without a zombie task
144
151
  let commands;
145
152
  try {
146
- commands = await loadCommands(CONFIG_PATH);
153
+ commands = await loadCommands(configPath);
147
154
  }
148
155
  catch (err) {
149
156
  throw new Error(`Error loading config: ${err.message}`);
@@ -159,6 +166,8 @@ export function registerTaskExecute(server, logger, activeExecutions) {
159
166
  : 300_000,
160
167
  pollInterval: 1000,
161
168
  });
169
+ // Notify TUI after task creation
170
+ resolvedTui?.addExecution(task.taskId, commandName, resolvedAgentId, resolvedProjectKey);
162
171
  // Compose abort signal from cancellation + optional timeout
163
172
  const ac = new AbortController();
164
173
  activeExecutions.set(task.taskId, ac);
@@ -187,6 +196,7 @@ export function registerTaskExecute(server, logger, activeExecutions) {
187
196
  server,
188
197
  logger,
189
198
  activeExecutions,
199
+ tui: resolvedTui,
190
200
  }).catch(() => {
191
201
  // errors are stored in the task store, not propagated here
192
202
  });