aem-ext-daemon 0.4.1 → 0.5.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/capabilities/fs.d.ts +5 -0
- package/dist/capabilities/fs.js +37 -0
- package/dist/capabilities/mcp.d.ts +38 -0
- package/dist/capabilities/mcp.js +162 -0
- package/dist/capabilities/shell.d.ts +6 -8
- package/dist/capabilities/shell.js +59 -142
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +3 -1
- package/dist/dispatcher.js +29 -3
- package/package.json +2 -1
|
@@ -17,5 +17,10 @@ export declare function readFile(filePath: string): string;
|
|
|
17
17
|
export declare function mkdir(dirPath: string): string;
|
|
18
18
|
/** Write content to a file. Creates parent directories if needed. */
|
|
19
19
|
export declare function writeFile(filePath: string, content: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Find all files recursively and return as flat JSON array of relative paths.
|
|
22
|
+
* Used for file search and @ mention autocomplete.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findFiles(dirPath: string, maxDepth?: number): string;
|
|
20
25
|
/** Recursive directory listing up to a max depth. */
|
|
21
26
|
export declare function listRecursive(dirPath: string, maxDepth?: number): string;
|
package/dist/capabilities/fs.js
CHANGED
|
@@ -65,6 +65,43 @@ export function writeFile(filePath, content) {
|
|
|
65
65
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
66
66
|
return `Written ${content.length} bytes to ${filePath}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Find all files recursively and return as flat JSON array of relative paths.
|
|
70
|
+
* Used for file search and @ mention autocomplete.
|
|
71
|
+
*/
|
|
72
|
+
export function findFiles(dirPath, maxDepth = 8) {
|
|
73
|
+
const SKIP = new Set([
|
|
74
|
+
"node_modules", "__pycache__", "dist", "build", ".git",
|
|
75
|
+
".next", ".cache", "coverage", ".turbo", ".parcel-cache",
|
|
76
|
+
]);
|
|
77
|
+
const MAX_FILES = 5000;
|
|
78
|
+
const files = [];
|
|
79
|
+
function walk(dir, rel, depth) {
|
|
80
|
+
if (depth > maxDepth || files.length >= MAX_FILES)
|
|
81
|
+
return;
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const filtered = entries.filter((e) => !e.name.startsWith(".") && !SKIP.has(e.name));
|
|
90
|
+
for (const entry of filtered) {
|
|
91
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
walk(path.join(dir, entry.name), relPath, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
files.push(relPath);
|
|
97
|
+
if (files.length >= MAX_FILES)
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
walk(dirPath, "", 0);
|
|
103
|
+
return JSON.stringify(files);
|
|
104
|
+
}
|
|
68
105
|
/** Recursive directory listing up to a max depth. */
|
|
69
106
|
export function listRecursive(dirPath, maxDepth = 3) {
|
|
70
107
|
const result = [];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP capabilities — manage stdio-based MCP server processes on the local machine.
|
|
3
|
+
*
|
|
4
|
+
* Each server is a child process communicating via stdin/stdout using the MCP protocol.
|
|
5
|
+
* The daemon manages the lifecycle and proxies tool calls from the cloud server.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Start a stdio MCP server process and discover its tools.
|
|
9
|
+
*
|
|
10
|
+
* @param config Server configuration
|
|
11
|
+
* @returns JSON string with status and tool list
|
|
12
|
+
*/
|
|
13
|
+
export declare function startServer(config: {
|
|
14
|
+
id: string;
|
|
15
|
+
command: string;
|
|
16
|
+
args?: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
}): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Stop a running MCP server and kill its process.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stopServer(id: string): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Call a tool on a running MCP server.
|
|
25
|
+
*/
|
|
26
|
+
export declare function callTool(serverId: string, toolName: string, args: Record<string, unknown>): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* List tools for a running MCP server.
|
|
29
|
+
*/
|
|
30
|
+
export declare function listTools(serverId: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Get status of all running MCP servers.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getStatus(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Stop all running MCP servers. Called during daemon shutdown.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stopAll(): Promise<void>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP capabilities — manage stdio-based MCP server processes on the local machine.
|
|
3
|
+
*
|
|
4
|
+
* Each server is a child process communicating via stdin/stdout using the MCP protocol.
|
|
5
|
+
* The daemon manages the lifecycle and proxies tool calls from the cloud server.
|
|
6
|
+
*/
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
|
+
import { logInfo, logError } from "../logger.js";
|
|
10
|
+
// Active MCP server processes, keyed by server ID
|
|
11
|
+
const servers = new Map();
|
|
12
|
+
/**
|
|
13
|
+
* Start a stdio MCP server process and discover its tools.
|
|
14
|
+
*
|
|
15
|
+
* @param config Server configuration
|
|
16
|
+
* @returns JSON string with status and tool list
|
|
17
|
+
*/
|
|
18
|
+
export async function startServer(config) {
|
|
19
|
+
const { id, command, args, env } = config;
|
|
20
|
+
// Stop existing instance if running
|
|
21
|
+
if (servers.has(id)) {
|
|
22
|
+
await stopServer(id);
|
|
23
|
+
}
|
|
24
|
+
logInfo(`MCP: starting server "${id}" — ${command} ${(args || []).join(" ")}`);
|
|
25
|
+
const transport = new StdioClientTransport({
|
|
26
|
+
command,
|
|
27
|
+
args: args || [],
|
|
28
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
29
|
+
});
|
|
30
|
+
const client = new Client({
|
|
31
|
+
name: "aem-ext-daemon",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
await client.connect(transport);
|
|
36
|
+
// Discover tools
|
|
37
|
+
const result = await client.listTools();
|
|
38
|
+
const tools = (result.tools || []).map((t) => ({
|
|
39
|
+
name: t.name,
|
|
40
|
+
description: t.description || "",
|
|
41
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} },
|
|
42
|
+
}));
|
|
43
|
+
// Try to get the PID from the transport's subprocess
|
|
44
|
+
let pid;
|
|
45
|
+
try {
|
|
46
|
+
// The StdioClientTransport exposes the child process internally
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
const tp = transport;
|
|
49
|
+
pid = tp._process?.pid || tp.process?.pid;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// PID not available — not critical
|
|
53
|
+
}
|
|
54
|
+
const managed = { id, client, transport, tools, pid };
|
|
55
|
+
servers.set(id, managed);
|
|
56
|
+
logInfo(`MCP: server "${id}" started — ${tools.length} tools discovered${pid ? ` (PID ${pid})` : ""}`);
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
running: true,
|
|
59
|
+
serverId: id,
|
|
60
|
+
pid,
|
|
61
|
+
toolCount: tools.length,
|
|
62
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description })),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
logError("mcp:start", message, 0);
|
|
68
|
+
// Ensure cleanup on failure
|
|
69
|
+
try {
|
|
70
|
+
await client.close();
|
|
71
|
+
}
|
|
72
|
+
catch { /* ignore */ }
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
running: false,
|
|
75
|
+
serverId: id,
|
|
76
|
+
error: message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop a running MCP server and kill its process.
|
|
82
|
+
*/
|
|
83
|
+
export async function stopServer(id) {
|
|
84
|
+
const server = servers.get(id);
|
|
85
|
+
if (!server) {
|
|
86
|
+
return JSON.stringify({ stopped: true, serverId: id, wasRunning: false });
|
|
87
|
+
}
|
|
88
|
+
logInfo(`MCP: stopping server "${id}"`);
|
|
89
|
+
try {
|
|
90
|
+
await server.client.close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Force-kill if close doesn't work
|
|
94
|
+
if (server.pid) {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(server.pid, "SIGTERM");
|
|
97
|
+
}
|
|
98
|
+
catch { /* already dead */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
servers.delete(id);
|
|
102
|
+
return JSON.stringify({ stopped: true, serverId: id, wasRunning: true });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Call a tool on a running MCP server.
|
|
106
|
+
*/
|
|
107
|
+
export async function callTool(serverId, toolName, args) {
|
|
108
|
+
const server = servers.get(serverId);
|
|
109
|
+
if (!server) {
|
|
110
|
+
throw new Error(`MCP server "${serverId}" is not running`);
|
|
111
|
+
}
|
|
112
|
+
const result = await server.client.callTool({
|
|
113
|
+
name: toolName,
|
|
114
|
+
arguments: args,
|
|
115
|
+
});
|
|
116
|
+
// Extract text from content blocks
|
|
117
|
+
if (result.content && Array.isArray(result.content)) {
|
|
118
|
+
return result.content
|
|
119
|
+
.map((block) => {
|
|
120
|
+
if (block.type === "text")
|
|
121
|
+
return block.text || "";
|
|
122
|
+
return JSON.stringify(block);
|
|
123
|
+
})
|
|
124
|
+
.join("\n");
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(result);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* List tools for a running MCP server.
|
|
130
|
+
*/
|
|
131
|
+
export function listTools(serverId) {
|
|
132
|
+
const server = servers.get(serverId);
|
|
133
|
+
if (!server) {
|
|
134
|
+
return JSON.stringify({ serverId, running: false, tools: [] });
|
|
135
|
+
}
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
serverId,
|
|
138
|
+
running: true,
|
|
139
|
+
tools: server.tools,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get status of all running MCP servers.
|
|
144
|
+
*/
|
|
145
|
+
export function getStatus() {
|
|
146
|
+
const statuses = Array.from(servers.entries()).map(([id, server]) => ({
|
|
147
|
+
serverId: id,
|
|
148
|
+
running: true,
|
|
149
|
+
pid: server.pid,
|
|
150
|
+
toolCount: server.tools.length,
|
|
151
|
+
}));
|
|
152
|
+
return JSON.stringify({ servers: statuses });
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Stop all running MCP servers. Called during daemon shutdown.
|
|
156
|
+
*/
|
|
157
|
+
export async function stopAll() {
|
|
158
|
+
const ids = Array.from(servers.keys());
|
|
159
|
+
for (const id of ids) {
|
|
160
|
+
await stopServer(id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -18,13 +18,11 @@ export declare function checkAio(): string;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function runAio(args: string[], cwd: string, requestId: string, connection: DaemonConnection): Promise<string>;
|
|
20
20
|
/**
|
|
21
|
-
* Start an interactive command
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Start an interactive command by opening a real terminal window on the
|
|
22
|
+
* user's machine. Returns immediately with instructions — the agent
|
|
23
|
+
* should poll for completion (e.g., check for .aio file).
|
|
24
|
+
*
|
|
25
|
+
* This is needed because inquirer-based CLIs (like `aio app create`)
|
|
26
|
+
* require a real TTY for their interactive prompts.
|
|
24
27
|
*/
|
|
25
28
|
export declare function spawnInteractive(command: string, cwd: string): Promise<string>;
|
|
26
|
-
/**
|
|
27
|
-
* Send input to the active interactive process. Waits for the process
|
|
28
|
-
* to produce more output (or go quiet again), then returns the new output.
|
|
29
|
-
*/
|
|
30
|
-
export declare function writeToProcess(input: string): Promise<string>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { execSync, spawn } from "node:child_process";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
12
13
|
const EXEC_TIMEOUT = 120_000; // 2 minutes
|
|
13
14
|
const MAX_BUFFER = 5 * 1024 * 1024; // 5MB
|
|
14
15
|
/** Resolve the user's login shell (falls back to /bin/sh). */
|
|
@@ -146,154 +147,70 @@ export function runAio(args, cwd, requestId, connection) {
|
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
// ─── Interactive process support ────────────────────────────
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
/** The single active interactive process (one per daemon). */
|
|
152
|
-
let activeProcess = null;
|
|
153
|
-
/** How long to wait for more output before assuming the process is waiting for input. */
|
|
154
|
-
const SILENCE_TIMEOUT = 3_000;
|
|
155
|
-
/** Strip ANSI escape codes from PTY output for clean text. */
|
|
156
|
-
function stripAnsi(text) {
|
|
157
|
-
// eslint-disable-next-line no-control-regex
|
|
158
|
-
return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "")
|
|
159
|
-
.replace(/\r\n/g, "\n")
|
|
160
|
-
.replace(/\r/g, "\n");
|
|
161
|
-
}
|
|
150
|
+
// Opens interactive commands in a real terminal window since
|
|
151
|
+
// inquirer-based CLIs require a TTY for their prompts.
|
|
162
152
|
/**
|
|
163
|
-
* Start an interactive command
|
|
164
|
-
*
|
|
165
|
-
*
|
|
153
|
+
* Start an interactive command by opening a real terminal window on the
|
|
154
|
+
* user's machine. Returns immediately with instructions — the agent
|
|
155
|
+
* should poll for completion (e.g., check for .aio file).
|
|
156
|
+
*
|
|
157
|
+
* This is needed because inquirer-based CLIs (like `aio app create`)
|
|
158
|
+
* require a real TTY for their interactive prompts.
|
|
166
159
|
*/
|
|
167
160
|
export function spawnInteractive(command, cwd) {
|
|
168
|
-
// Kill any existing interactive process
|
|
169
|
-
if (activeProcess && !activeProcess.done) {
|
|
170
|
-
activeProcess.child.kill("SIGTERM");
|
|
171
|
-
}
|
|
172
|
-
activeProcess = null;
|
|
173
161
|
if (!fs.existsSync(cwd)) {
|
|
174
162
|
fs.mkdirSync(cwd, { recursive: true });
|
|
175
163
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
child.stderr?.on("data", (chunk) => {
|
|
219
|
-
proc.buffer += chunk.toString();
|
|
220
|
-
resetTimer();
|
|
221
|
-
});
|
|
222
|
-
child.on("close", (code) => {
|
|
223
|
-
proc.done = true;
|
|
224
|
-
proc.exitCode = code;
|
|
225
|
-
clearTimeout(silenceTimer);
|
|
226
|
-
proc.buffer += `\n[Process exited with code ${code ?? 1}]`;
|
|
227
|
-
flushAndReturn();
|
|
228
|
-
});
|
|
229
|
-
child.on("error", (err) => {
|
|
230
|
-
proc.done = true;
|
|
231
|
-
clearTimeout(silenceTimer);
|
|
232
|
-
proc.buffer += `\n[Error: ${err.message}]`;
|
|
233
|
-
flushAndReturn();
|
|
234
|
-
});
|
|
235
|
-
// Start the silence timer
|
|
236
|
-
resetTimer();
|
|
237
|
-
// Overall timeout: 5 minutes
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
if (!proc.done) {
|
|
240
|
-
child.kill("SIGTERM");
|
|
241
|
-
proc.done = true;
|
|
242
|
-
proc.buffer += "\n[Timed out after 5 minutes]";
|
|
243
|
-
flushAndReturn();
|
|
164
|
+
const platform = os.platform();
|
|
165
|
+
if (platform === "darwin") {
|
|
166
|
+
// macOS: write a temp script and open it in Terminal.app
|
|
167
|
+
const scriptPath = path.join(os.tmpdir(), `aio-interactive-${Date.now()}.sh`);
|
|
168
|
+
fs.writeFileSync(scriptPath, [
|
|
169
|
+
"#!/bin/bash",
|
|
170
|
+
`cd "${cwd}"`,
|
|
171
|
+
command,
|
|
172
|
+
'echo ""',
|
|
173
|
+
'echo "✓ Done! You can close this terminal window."',
|
|
174
|
+
'read -p "Press Enter to close..."',
|
|
175
|
+
].join("\n"), { mode: 0o755 });
|
|
176
|
+
try {
|
|
177
|
+
execSync(`open -a Terminal.app "${scriptPath}"`, {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
timeout: 10_000,
|
|
180
|
+
});
|
|
181
|
+
return Promise.resolve(`[INTERACTIVE] Opened a new Terminal window to run: ${command}\n` +
|
|
182
|
+
`Working directory: ${cwd}\n\n` +
|
|
183
|
+
`The user needs to complete the interactive prompts in that Terminal window.\n` +
|
|
184
|
+
`After they finish, check for the .aio file to confirm: ls -la .aio\n` +
|
|
185
|
+
`DO NOT proceed until the .aio file exists.`);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
return Promise.resolve(`Error opening Terminal: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (platform === "linux") {
|
|
192
|
+
// Try common Linux terminal emulators
|
|
193
|
+
const terminals = ["gnome-terminal", "xfce4-terminal", "konsole", "xterm"];
|
|
194
|
+
for (const term of terminals) {
|
|
195
|
+
try {
|
|
196
|
+
execSync(`which ${term}`, { encoding: "utf-8" });
|
|
197
|
+
execSync(`${term} -- bash -c 'cd "${cwd}" && ${command} && echo Done && read -p "Press Enter..."'`, {
|
|
198
|
+
encoding: "utf-8",
|
|
199
|
+
timeout: 10_000,
|
|
200
|
+
});
|
|
201
|
+
return Promise.resolve(`[INTERACTIVE] Opened ${term} to run: ${command}\n` +
|
|
202
|
+
`Working directory: ${cwd}\n\n` +
|
|
203
|
+
`The user needs to complete the interactive prompts in that terminal.\n` +
|
|
204
|
+
`After they finish, check for the .aio file: ls -la .aio`);
|
|
244
205
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
* Send input to the active interactive process. Waits for the process
|
|
250
|
-
* to produce more output (or go quiet again), then returns the new output.
|
|
251
|
-
*/
|
|
252
|
-
export function writeToProcess(input) {
|
|
253
|
-
if (!activeProcess || activeProcess.done) {
|
|
254
|
-
return Promise.resolve(activeProcess
|
|
255
|
-
? `[Process already exited with code ${activeProcess.exitCode}]`
|
|
256
|
-
: "[No interactive process running. Use interactive_bash to start one.]");
|
|
206
|
+
catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
257
210
|
}
|
|
258
|
-
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (resolved)
|
|
264
|
-
return;
|
|
265
|
-
resolved = true;
|
|
266
|
-
clearTimeout(silenceTimer);
|
|
267
|
-
proc.child.stdout?.off("data", onData);
|
|
268
|
-
proc.child.stderr?.off("data", onData);
|
|
269
|
-
proc.child.off("close", onClose);
|
|
270
|
-
resolve(output);
|
|
271
|
-
};
|
|
272
|
-
const resetTimer = () => {
|
|
273
|
-
clearTimeout(silenceTimer);
|
|
274
|
-
silenceTimer = setTimeout(() => {
|
|
275
|
-
if (!proc.done) {
|
|
276
|
-
const output = stripAnsi(proc.buffer);
|
|
277
|
-
proc.buffer = "";
|
|
278
|
-
finish(output || "(waiting for input...)");
|
|
279
|
-
}
|
|
280
|
-
}, SILENCE_TIMEOUT);
|
|
281
|
-
};
|
|
282
|
-
const onData = () => resetTimer();
|
|
283
|
-
const onClose = () => {
|
|
284
|
-
// Small delay to capture any final output
|
|
285
|
-
setTimeout(() => {
|
|
286
|
-
const output = stripAnsi(proc.buffer);
|
|
287
|
-
proc.buffer = "";
|
|
288
|
-
finish(output || "(no output)");
|
|
289
|
-
}, 200);
|
|
290
|
-
};
|
|
291
|
-
proc.child.stdout?.on("data", onData);
|
|
292
|
-
proc.child.stderr?.on("data", onData);
|
|
293
|
-
proc.child.once("close", onClose);
|
|
294
|
-
// Write the input
|
|
295
|
-
proc.child.stdin?.write(input + "\n");
|
|
296
|
-
// Start silence timer
|
|
297
|
-
resetTimer();
|
|
298
|
-
});
|
|
211
|
+
// Fallback: tell the user to run it manually
|
|
212
|
+
return Promise.resolve(`[INTERACTIVE] This command requires interactive input and must be run in a terminal.\n\n` +
|
|
213
|
+
`Please ask the user to open their terminal and run:\n\n` +
|
|
214
|
+
` cd "${cwd}"\n ${command}\n\n` +
|
|
215
|
+
`After they finish, check for the .aio file: ls -la .aio`);
|
|
299
216
|
}
|
package/dist/daemon.d.ts
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -17,6 +17,7 @@ import { DaemonConnection } from "./connection.js";
|
|
|
17
17
|
import { ensureIdentity, getWorkspaceRoot, setWorkspaceRoot } from "./identity.js";
|
|
18
18
|
import { generatePairCode, displayPairCode } from "./pairing.js";
|
|
19
19
|
import { dispatch } from "./dispatcher.js";
|
|
20
|
+
import { stopAll as stopAllMcpServers } from "./capabilities/mcp.js";
|
|
20
21
|
import { getLogFilePath } from "./logger.js";
|
|
21
22
|
// Read version from package.json so it stays in sync automatically
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -64,8 +65,9 @@ export class Daemon {
|
|
|
64
65
|
this.connection.connect();
|
|
65
66
|
}
|
|
66
67
|
/** Stop the daemon gracefully. */
|
|
67
|
-
stop() {
|
|
68
|
+
async stop() {
|
|
68
69
|
console.log("\n Shutting down...");
|
|
70
|
+
await stopAllMcpServers();
|
|
69
71
|
this.connection.disconnect();
|
|
70
72
|
}
|
|
71
73
|
handleStatus(status) {
|
package/dist/dispatcher.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fsCap from "./capabilities/fs.js";
|
|
|
11
11
|
import * as gitCap from "./capabilities/git.js";
|
|
12
12
|
import * as shellCap from "./capabilities/shell.js";
|
|
13
13
|
import * as skillsCap from "./capabilities/skills.js";
|
|
14
|
+
import * as mcpCap from "./capabilities/mcp.js";
|
|
14
15
|
/**
|
|
15
16
|
* Validate that a given path is within the workspace root.
|
|
16
17
|
* Prevents path traversal attacks.
|
|
@@ -77,6 +78,12 @@ async function dispatchInner(command, payload, connection) {
|
|
|
77
78
|
: getWorkspaceRoot();
|
|
78
79
|
return fsCap.listRecursive(target, payload.depth);
|
|
79
80
|
}
|
|
81
|
+
case "fs:find": {
|
|
82
|
+
const target = payload.path
|
|
83
|
+
? validatePath(payload.path)
|
|
84
|
+
: getWorkspaceRoot();
|
|
85
|
+
return fsCap.findFiles(target, payload.depth);
|
|
86
|
+
}
|
|
80
87
|
// ─── Workspace ──────────────────────────────────────
|
|
81
88
|
case "workspace:get": {
|
|
82
89
|
return { workspaceRoot: getWorkspaceRoot() };
|
|
@@ -119,9 +126,7 @@ async function dispatchInner(command, payload, connection) {
|
|
|
119
126
|
: getWorkspaceRoot();
|
|
120
127
|
return shellCap.spawnInteractive(payload.command, cwd);
|
|
121
128
|
}
|
|
122
|
-
|
|
123
|
-
return shellCap.writeToProcess(payload.input);
|
|
124
|
-
}
|
|
129
|
+
// shell:stdin removed — interactive commands now open a terminal window
|
|
125
130
|
// ─── Skills ────────────────────────────────────────
|
|
126
131
|
case "skills:sync": {
|
|
127
132
|
const workspace = getWorkspaceRoot();
|
|
@@ -135,6 +140,27 @@ async function dispatchInner(command, payload, connection) {
|
|
|
135
140
|
throw new Error("No workspace root configured.");
|
|
136
141
|
return skillsCap.seedSkills(workspace);
|
|
137
142
|
}
|
|
143
|
+
// ─── MCP Servers ─────────────────────────────────────
|
|
144
|
+
case "mcp:start": {
|
|
145
|
+
return mcpCap.startServer({
|
|
146
|
+
id: payload.id,
|
|
147
|
+
command: payload.command,
|
|
148
|
+
args: payload.args,
|
|
149
|
+
env: payload.env,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
case "mcp:stop": {
|
|
153
|
+
return mcpCap.stopServer(payload.serverId);
|
|
154
|
+
}
|
|
155
|
+
case "mcp:call": {
|
|
156
|
+
return mcpCap.callTool(payload.serverId, payload.toolName, payload.args || {});
|
|
157
|
+
}
|
|
158
|
+
case "mcp:tools": {
|
|
159
|
+
return mcpCap.listTools(payload.serverId);
|
|
160
|
+
}
|
|
161
|
+
case "mcp:status": {
|
|
162
|
+
return mcpCap.getStatus();
|
|
163
|
+
}
|
|
138
164
|
default:
|
|
139
165
|
throw new Error(`Unknown command: ${command}`);
|
|
140
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aem-ext-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local daemon for AEM Extension Builder — connects your machine to the cloud UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "npm run build"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
18
19
|
"conf": "^13.0.1",
|
|
19
20
|
"qrcode-terminal": "^0.12.0",
|
|
20
21
|
"ws": "^8.18.0"
|