@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/README.md +77 -38
- package/dist/cli.js +29 -0
- package/dist/cli.test.js +45 -0
- package/dist/config.js +8 -1
- package/dist/config.test.js +31 -1
- package/dist/index.js +292 -47
- package/dist/logger.js +29 -16
- package/dist/registry.js +80 -0
- package/dist/registry.test.js +107 -0
- package/dist/server.js +20 -10
- package/dist/task.js +14 -4
- package/dist/tui.js +517 -0
- package/package.json +4 -2
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|