mouaif 0.3.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/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/mouaif.js +281 -0
- package/frontend/dist/assets/AgentFilePicker-CcKLJorU.js +1 -0
- package/frontend/dist/assets/CliModal-Hs5phmNZ.js +7 -0
- package/frontend/dist/assets/DictationPage-BI23lp42.js +2 -0
- package/frontend/dist/assets/FileEditor-DDl31c6d.js +2 -0
- package/frontend/dist/assets/GitModal-3EC_gpJ5.js +2 -0
- package/frontend/dist/assets/Inspector-Ba3R1w04.js +73 -0
- package/frontend/dist/assets/SettingsAbout-bvZGDEDw.js +1 -0
- package/frontend/dist/assets/SettingsActions-Dk6WX9jv.js +1 -0
- package/frontend/dist/assets/SettingsAgents-BNV0MgDB.js +1 -0
- package/frontend/dist/assets/SettingsDefaults-DbMmQbzc.js +1 -0
- package/frontend/dist/assets/SettingsHiddenContent-BZ2sloH1.js +1 -0
- package/frontend/dist/assets/SettingsMcp-DOrfbQd1.js +1 -0
- package/frontend/dist/assets/SettingsMcpEdit-BGMQ2CWC.js +3 -0
- package/frontend/dist/assets/SettingsMcpRegistry-BywXee_A.js +1 -0
- package/frontend/dist/assets/SettingsNotifications-B0LEs11a.js +1 -0
- package/frontend/dist/assets/SettingsPricing-BAg33iVF.js +1 -0
- package/frontend/dist/assets/SettingsProject-DNrKhCcZ.js +14 -0
- package/frontend/dist/assets/SettingsProjects-IqkBfDcm.js +1 -0
- package/frontend/dist/assets/SettingsPrompts-BgeiASuk.js +1 -0
- package/frontend/dist/assets/SettingsProviders-k0xJN0IK.js +1 -0
- package/frontend/dist/assets/SettingsTags-B5kjFdQi.js +1 -0
- package/frontend/dist/assets/agentNavigation-BiiCpFz5.js +1 -0
- package/frontend/dist/assets/codemirror-Bp6CUUFk.js +30 -0
- package/frontend/dist/assets/index-BGvI4n0T.js +61 -0
- package/frontend/dist/assets/index-Bgg1gnDf.css +1 -0
- package/frontend/dist/assets/index-C1sQFIC-.css +1 -0
- package/frontend/dist/assets/index-CANPYzQg.css +1 -0
- package/frontend/dist/assets/index-Crn1LdzK.css +1 -0
- package/frontend/dist/assets/index-FbCWDPiB.css +1 -0
- package/frontend/dist/assets/projectQS-D1cSZ7Gr.js +1 -0
- package/frontend/dist/assets/virtual-list-6H9b4K51.js +1 -0
- package/frontend/dist/icons/favicon-32.png +0 -0
- package/frontend/dist/icons/icon-180-apple.png +0 -0
- package/frontend/dist/icons/icon-192.png +0 -0
- package/frontend/dist/icons/icon-512.png +0 -0
- package/frontend/dist/icons/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +83 -0
- package/frontend/dist/manifest.webmanifest +33 -0
- package/frontend/dist/sw.js +482 -0
- package/package.json +98 -0
- package/scripts/patch-zimmerframe.js +58 -0
- package/src/access-auth.js +515 -0
- package/src/agentFeatures.js +294 -0
- package/src/agentFiles.js +164 -0
- package/src/agentSkills.js +147 -0
- package/src/agents.js +230 -0
- package/src/ai-chat.js +21 -0
- package/src/ai-endpoints.js +1880 -0
- package/src/ai-stream.js +2048 -0
- package/src/ai.js +68 -0
- package/src/auth.js +391 -0
- package/src/chatdb.js +816 -0
- package/src/chats.js +275 -0
- package/src/custom-actions.js +65 -0
- package/src/files.js +431 -0
- package/src/hideFileContent.js +327 -0
- package/src/http-server.js +535 -0
- package/src/index.js +15 -0
- package/src/inspector.js +731 -0
- package/src/inspectorProfiles.js +503 -0
- package/src/live-chat.js +107 -0
- package/src/mcp.js +1517 -0
- package/src/messages.js +238 -0
- package/src/modelList.js +137 -0
- package/src/notifications.js +52 -0
- package/src/oauth-anthropic.js +280 -0
- package/src/oauth-github-copilot.js +417 -0
- package/src/oauth-mcp.js +216 -0
- package/src/oauth-openrouter.js +285 -0
- package/src/package-version.js +20 -0
- package/src/projects.js +285 -0
- package/src/promptProfiles.js +256 -0
- package/src/prompts.js +384 -0
- package/src/providerShapes.js +44 -0
- package/src/providers/base.js +41 -0
- package/src/providers/index.js +25 -0
- package/src/push.js +315 -0
- package/src/qr.js +192 -0
- package/src/restart.js +47 -0
- package/src/server-handlers-access.js +306 -0
- package/src/server-handlers-actions.js +100 -0
- package/src/server-handlers-ai.js +248 -0
- package/src/server-handlers-auth.js +273 -0
- package/src/server-handlers-chats.js +1436 -0
- package/src/server-handlers-git.js +467 -0
- package/src/server-handlers-mcp-oauth.js +56 -0
- package/src/server-handlers-misc.js +783 -0
- package/src/server-handlers-projects.js +289 -0
- package/src/server-handlers-prompts.js +259 -0
- package/src/server-handlers-push.js +102 -0
- package/src/server-handlers-settings.js +406 -0
- package/src/server-handlers-tools.js +654 -0
- package/src/server-handlers-transcribe.js +399 -0
- package/src/server-shared.js +780 -0
- package/src/server-web-static.js +191 -0
- package/src/settings.js +898 -0
- package/src/statusBar.js +541 -0
- package/src/tags.js +414 -0
- package/src/toolFeedback.js +225 -0
- package/src/tools/ask.js +154 -0
- package/src/tools/authorization.js +932 -0
- package/src/tools/files.js +1150 -0
- package/src/tools/progress.js +71 -0
- package/src/tools/restart.js +32 -0
- package/src/tools/searchEngine.js +957 -0
- package/src/tools/shell.js +341 -0
- package/src/tools/subagent.js +47 -0
- package/src/tools/task.js +234 -0
- package/src/tools/webpreview.js +448 -0
- package/src/trace.js +103 -0
- package/src/transcribe.js +683 -0
- package/src/usage.js +389 -0
- package/src/util.js +151 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Native tool REST handlers (shell, subagent, tool list). Extracted
|
|
4
|
+
// from the original single-file http-server.js. Shared helpers live in
|
|
5
|
+
// src/server-shared.js.
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sendJSON,
|
|
9
|
+
qs,
|
|
10
|
+
readJsonOr400,
|
|
11
|
+
resolveModel,
|
|
12
|
+
settings,
|
|
13
|
+
chats,
|
|
14
|
+
messages,
|
|
15
|
+
usage,
|
|
16
|
+
mcp,
|
|
17
|
+
shellTool,
|
|
18
|
+
ai,
|
|
19
|
+
broadcast
|
|
20
|
+
} = require('./server-shared.js');
|
|
21
|
+
|
|
22
|
+
// ---- Interactive CLI session ------------------------------------------------
|
|
23
|
+
//
|
|
24
|
+
// A single persistent command-line child per project (the platform shell:
|
|
25
|
+
// cmd.exe on Windows, the user's $SHELL or /bin/sh on POSIX) with
|
|
26
|
+
// stdin/stdout/stderr piped over HTTP. All commands run in the project
|
|
27
|
+
// directory ("default path to the project"). The session is created with
|
|
28
|
+
// `windowsHide` and no window is ever shown; output streaming rides the
|
|
29
|
+
// SSE endpoint (see startCliSession below).
|
|
30
|
+
//
|
|
31
|
+
// The child is intentionally spawned WITHOUT a TTY (pipe stdio). Commands
|
|
32
|
+
// that require an interactive TTY (REPLs, `cmd.exe` interactive prompts
|
|
33
|
+
// like `del` confirmation) will fail or exit immediately — a documented
|
|
34
|
+
// limitation, same as the native `shell` tool. Everything non-interactive
|
|
35
|
+
// works exactly like a real Command Prompt.
|
|
36
|
+
|
|
37
|
+
const { spawn } = require('node:child_process');
|
|
38
|
+
|
|
39
|
+
const cliSessions = new Map(); // projectDir -> { child, projectDir, id, startedAt }
|
|
40
|
+
|
|
41
|
+
// Reap every live CLI child on parent exit so a server shutdown never
|
|
42
|
+
// leaves orphaned cmd.exe processes behind (same pattern as the native
|
|
43
|
+
// shell tool's liveChildren set).
|
|
44
|
+
let cliExitHooked = false;
|
|
45
|
+
function hookCliExit() {
|
|
46
|
+
if (cliExitHooked) return;
|
|
47
|
+
cliExitHooked = true;
|
|
48
|
+
const reap = () => {
|
|
49
|
+
for (const s of cliSessions.values()) {
|
|
50
|
+
try { if (s.child && !s.child.killed) s.child.kill(); } catch { /* already gone */ }
|
|
51
|
+
}
|
|
52
|
+
cliSessions.clear();
|
|
53
|
+
};
|
|
54
|
+
process.on('exit', reap);
|
|
55
|
+
process.on('SIGINT', () => { reap(); process.exit(130); });
|
|
56
|
+
process.on('SIGTERM', () => { reap(); process.exit(143); });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cliShellMeta() {
|
|
60
|
+
// Default grid for a PTY-backed session. The modal renders plain text and
|
|
61
|
+
// does not yet report its own size, so this is a sensible fixed terminal
|
|
62
|
+
// size rather than the browser viewport.
|
|
63
|
+
const cols = 100;
|
|
64
|
+
const rows = 30;
|
|
65
|
+
if (process.platform === 'win32') {
|
|
66
|
+
// Windows Terminal Detection: if the session's env already has a WSL
|
|
67
|
+
// or PowerShell default, use it; otherwise cmd.exe (the classic prompt).
|
|
68
|
+
return {
|
|
69
|
+
exe: process.env.ComSpec || 'cmd.exe',
|
|
70
|
+
args: [],
|
|
71
|
+
label: 'Command Prompt (cmd.exe)',
|
|
72
|
+
windows: true,
|
|
73
|
+
cols,
|
|
74
|
+
rows
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const shellPath = process.env.SHELL || '/bin/sh';
|
|
78
|
+
return {
|
|
79
|
+
exe: shellPath,
|
|
80
|
+
args: [],
|
|
81
|
+
label: 'Shell (' + shellPath + ')',
|
|
82
|
+
windows: false,
|
|
83
|
+
cols,
|
|
84
|
+
rows
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Load node-pty once. It is an optional native dependency: a platform with
|
|
89
|
+
// no prebuilt binary (and no C++ toolchain) still installs mouaif, and the
|
|
90
|
+
// session silently falls back to the piped spawn below. `require` is
|
|
91
|
+
// wrapped so a missing/broken addon is a degraded mode, not a crash.
|
|
92
|
+
let ptyModule = null;
|
|
93
|
+
let ptyLoadAttempted = false;
|
|
94
|
+
function loadPty() {
|
|
95
|
+
if (ptyLoadAttempted) return ptyModule;
|
|
96
|
+
ptyLoadAttempted = true;
|
|
97
|
+
try {
|
|
98
|
+
ptyModule = require('node-pty');
|
|
99
|
+
} catch {
|
|
100
|
+
ptyModule = null;
|
|
101
|
+
}
|
|
102
|
+
return ptyModule;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Start the persistent session (idempotent) and return the session handle.
|
|
106
|
+
//
|
|
107
|
+
// The session runs on a pseudo-terminal when node-pty is available. That
|
|
108
|
+
// matters for prompting programs: over pipes (`stdio: ['pipe', ...]`) the
|
|
109
|
+
// child's stdin is not a TTY, so a program that asks a question either gets
|
|
110
|
+
// an immediate EOF or refuses to prompt at all — `npm publish` under 2FA
|
|
111
|
+
// answers `EOTP` with a masked auth URL instead of asking for a code, and
|
|
112
|
+
// `read` returns an empty answer. With a PTY the prompt is written to the
|
|
113
|
+
// screen, the user types the answer into the modal's prompt line, and it is
|
|
114
|
+
// delivered to the still-running child.
|
|
115
|
+
//
|
|
116
|
+
// A PTY merges stdout and stderr into one stream, so `attachCliStream`
|
|
117
|
+
// labels every chunk `stdout`; there is no separate stderr channel to
|
|
118
|
+
// preserve. When the PTY is unavailable the old piped triple is used and
|
|
119
|
+
// stderr keeps its own channel.
|
|
120
|
+
function ensureCliSession(projectDir) {
|
|
121
|
+
const key = String(projectDir || '');
|
|
122
|
+
const existing = cliSessions.get(key);
|
|
123
|
+
if (existing && existing.child && !existing.child.killed) return existing;
|
|
124
|
+
const meta = cliShellMeta();
|
|
125
|
+
const pty = loadPty();
|
|
126
|
+
let child;
|
|
127
|
+
let isPty = false;
|
|
128
|
+
if (pty && typeof pty.spawn === 'function') {
|
|
129
|
+
try {
|
|
130
|
+
// The shell is only interactive once it has a TTY, so ask for `-i`
|
|
131
|
+
// here rather than in cliShellMeta (the piped fallback must not: bash
|
|
132
|
+
// warns about job control with no TTY).
|
|
133
|
+
const ptyArgs = (!meta.windows && meta.args.indexOf('-i') === -1)
|
|
134
|
+
? meta.args.concat('-i')
|
|
135
|
+
: meta.args;
|
|
136
|
+
child = pty.spawn(meta.exe, ptyArgs, {
|
|
137
|
+
name: 'xterm-256color',
|
|
138
|
+
cols: meta.cols,
|
|
139
|
+
rows: meta.rows,
|
|
140
|
+
cwd: projectDir,
|
|
141
|
+
// The child inherits the server's env; force a colour-capable TERM
|
|
142
|
+
// so utilities that gate formatting on terminfo behave.
|
|
143
|
+
env: Object.assign({}, process.env, { TERM: process.env.TERM || 'xterm-256color' })
|
|
144
|
+
});
|
|
145
|
+
isPty = true;
|
|
146
|
+
} catch {
|
|
147
|
+
child = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!child) {
|
|
151
|
+
child = spawn(meta.exe, meta.args, {
|
|
152
|
+
cwd: projectDir,
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const session = {
|
|
158
|
+
id: 'cli_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7),
|
|
159
|
+
projectDir,
|
|
160
|
+
child,
|
|
161
|
+
startedAt: Date.now(),
|
|
162
|
+
// True when the child runs on a pseudo-terminal (see loadPty above).
|
|
163
|
+
pty: isPty,
|
|
164
|
+
// Record the platform so command writes use the correct line
|
|
165
|
+
// terminator: CRLF for cmd.exe on Windows, LF for sh/bash on POSIX.
|
|
166
|
+
windows: !!meta.windows
|
|
167
|
+
};
|
|
168
|
+
hookCliExit();
|
|
169
|
+
cliSessions.set(key, session);
|
|
170
|
+
// Reap on exit so a closed session doesn't leak. node-pty reports exit
|
|
171
|
+
// through `onExit`; a piped child uses the standard `exit` event.
|
|
172
|
+
if (isPty) child.onExit(() => { if (cliSessions.get(key) === session) cliSessions.delete(key); });
|
|
173
|
+
else child.on('exit', () => { if (cliSessions.get(key) === session) cliSessions.delete(key); });
|
|
174
|
+
return session;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function closeCliSession(projectDir) {
|
|
178
|
+
const key = String(projectDir || '');
|
|
179
|
+
const session = cliSessions.get(key);
|
|
180
|
+
if (!session) return false;
|
|
181
|
+
try {
|
|
182
|
+
if (session.child && !session.child.killed) session.child.kill();
|
|
183
|
+
} catch { /* already gone */ }
|
|
184
|
+
cliSessions.delete(key);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Write one command line to the session's stdin. Shared by the HTTP handler
|
|
189
|
+
// and the tests so the terminator rule lives in exactly one place: CRLF for
|
|
190
|
+
// cmd.exe on Windows, LF for sh/bash on POSIX. A PTY in canonical mode also
|
|
191
|
+
// accepts CR to submit, but LF is what the piped path needs and what the
|
|
192
|
+
// shell consumes cleanly under both.
|
|
193
|
+
//
|
|
194
|
+
// `raw` writes the text with no terminator: an interactive program waiting on
|
|
195
|
+
// a single key (a `y/n` confirmation, a pager, a TUI) needs the byte alone,
|
|
196
|
+
// where appending a newline would answer a *second* prompt.
|
|
197
|
+
function writeCliCommand(session, cmd, raw) {
|
|
198
|
+
if (!session || !session.child) return false;
|
|
199
|
+
const newline = session.windows ? '\r\n' : '\n';
|
|
200
|
+
const line = String(cmd) + (raw ? '' : newline);
|
|
201
|
+
if (session.pty) {
|
|
202
|
+
if (typeof session.child.write !== 'function') return false;
|
|
203
|
+
session.child.write(line);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (!session.child.stdin || !session.child.stdin.writable) return false;
|
|
207
|
+
session.child.stdin.write(line);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Forward a session's stdout/stderr to the SSE broadcast channel. The
|
|
212
|
+
// browser opens GET /events, receives the session id, and listens for
|
|
213
|
+
// `cli_output` frames tagged with that id.
|
|
214
|
+
function attachCliStream(session, broadcast) {
|
|
215
|
+
if (!session || !session.child || !broadcast) return;
|
|
216
|
+
const emit = (stream, d) => {
|
|
217
|
+
broadcast('cli_output', { id: session.id, stream, data: Buffer.isBuffer(d) ? d.toString('utf8') : String(d) });
|
|
218
|
+
};
|
|
219
|
+
if (session.pty) {
|
|
220
|
+
// A PTY merges stdout and stderr into one readable stream and reports
|
|
221
|
+
// the exit code through onExit (the child has no `exit` event). Every
|
|
222
|
+
// chunk is labelled stdout because the two channels are no longer
|
|
223
|
+
// distinguishable.
|
|
224
|
+
session.child.onData((d) => emit('stdout', d));
|
|
225
|
+
session.child.onExit(({ exitCode }) => emit('exit', String(exitCode)));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
session.child.stdout.on('data', (d) => emit('stdout', d));
|
|
229
|
+
session.child.stderr.on('data', (d) => emit('stderr', d));
|
|
230
|
+
session.child.on('exit', (code) => emit('exit', String(code)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- Tools API ---------------------------------------------------------------
|
|
234
|
+
//
|
|
235
|
+
// REST surface for the native tool set (shell, file tools).
|
|
236
|
+
// Routes:
|
|
237
|
+
// GET /api/tools/list?projectDir=<abs>
|
|
238
|
+
// Catalog of every tool the model can be advertised to use on
|
|
239
|
+
// this project: native shell + file tools, plus MCP-discovered
|
|
240
|
+
// tools whose server is currently running. The chat UI reads
|
|
241
|
+
// this to render the per-chat tool toggles shown below the
|
|
242
|
+
// system prompt on a brand-new chat.
|
|
243
|
+
// POST /api/tools/shell body: { projectDir, cmd, timeoutMs? }
|
|
244
|
+
// -> NDJSON stream (application/x-ndjson): `output` lines while the
|
|
245
|
+
// command runs, then `result` line with
|
|
246
|
+
// { ok, stdout, stderr, exitCode, durationMs } | { ok:false, error, code }
|
|
247
|
+
// The tool is off unless the project's resolved settings enable it
|
|
248
|
+
// (settings.tools.shell.enabled). A disabled project returns
|
|
249
|
+
// ETOOL_DISABLED with HTTP 403.
|
|
250
|
+
|
|
251
|
+
// pushNativeTool(tools, opts) — append one built-in tool entry to the
|
|
252
|
+
// catalog. All the single native tools advertise the same shape
|
|
253
|
+
// ({ name, kind: 'native', source, description }); only the module to
|
|
254
|
+
// load, its spec accessor and the fallback description differ, so the
|
|
255
|
+
// six repetitive try/catch + push blocks collapse into one loader.
|
|
256
|
+
// `opts.spec(mod)` returns the tool's `function` block (or undefined).
|
|
257
|
+
// If the module cannot be loaded the tool is silently omitted.
|
|
258
|
+
function pushNativeTool(tools, opts) {
|
|
259
|
+
try {
|
|
260
|
+
const mod = require(opts.load);
|
|
261
|
+
const fn = opts.spec(mod) || {};
|
|
262
|
+
tools.push({
|
|
263
|
+
name: opts.name,
|
|
264
|
+
kind: 'native',
|
|
265
|
+
source: opts.source,
|
|
266
|
+
description: fn.description || opts.fallback
|
|
267
|
+
});
|
|
268
|
+
} catch { /* module unavailable; omit */ }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function handleTools(req, res, parsed) {
|
|
272
|
+
const urlPath = parsed.pathname;
|
|
273
|
+
const method = req.method;
|
|
274
|
+
const q = parsed.query || {};
|
|
275
|
+
|
|
276
|
+
// GET /api/tools/list?projectDir=<abs>
|
|
277
|
+
// The catalog is read-only; it does not require a chat id. Native
|
|
278
|
+
// tools are always present; MCP tools are filtered to servers
|
|
279
|
+
// whose session is currently 'ready' (decisions §18: tools belong
|
|
280
|
+
// to a running session). A server that is not running is not
|
|
281
|
+
// listed — the user can start it from Settings → MCP, and a
|
|
282
|
+
// subsequent call will pick up the newly discovered tools.
|
|
283
|
+
if (urlPath === '/api/tools/list' && method === 'GET') {
|
|
284
|
+
const projectDir = qs(q, 'projectDir');
|
|
285
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
286
|
+
// A chat opening (or the mobile UI re-loading a project) should
|
|
287
|
+
// re-attach the MCP servers it needs. The Settings UI documents
|
|
288
|
+
// this as "open a chat that references a stopped server" — without
|
|
289
|
+
// it, the tool list comes back empty after a server restart.
|
|
290
|
+
// Wait for discovery so this response is authoritative. Returning
|
|
291
|
+
// before the servers finish starting makes a fresh .mcp.json
|
|
292
|
+
// configuration disappear from the first tool catalog (there may be
|
|
293
|
+
// no cache yet), and callers have no completion signal to know when
|
|
294
|
+
// to retry. ensureServersRunning isolates failures per server, so one
|
|
295
|
+
// broken MCP process does not fail the native-tool catalog.
|
|
296
|
+
await mcp.ensureServersRunning(projectDir).catch(() => []);
|
|
297
|
+
const tools = [];
|
|
298
|
+
pushNativeTool(tools, { load: './tools/shell.js', name: 'shell', source: 'shell', fallback: 'Run a shell command in the project directory.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
299
|
+
pushNativeTool(tools, { load: './tools/progress.js', name: 'report_progress', source: 'progress', fallback: 'Report real-time progress on a long-running operation.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
300
|
+
pushNativeTool(tools, { load: './tools/subagent.js', name: 'subagent', source: 'subagent', fallback: 'Delegate a focused task to a nested AI call.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
301
|
+
pushNativeTool(tools, { load: './agentFeatures.js', name: 'list_features', source: 'features', fallback: 'Describe mouaif feature state.', spec: (m) => m.LIST_FEATURES_SPEC && m.LIST_FEATURES_SPEC.function });
|
|
302
|
+
pushNativeTool(tools, { load: './tools/ask.js', name: 'ask_user', source: 'ask_user', fallback: 'Ask the user a structured question with options.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
303
|
+
pushNativeTool(tools, { load: './tools/task.js', name: 'task', source: 'task', fallback: 'Create, update, track progress on, and list structured tasks with subtasks.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
304
|
+
pushNativeTool(tools, { load: './tools/webpreview.js', name: 'webpreview', source: 'webpreview', fallback: 'Open a web URL in the debug Chrome and return a small screenshot of the page.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
305
|
+
pushNativeTool(tools, { load: './tools/restart.js', name: 'restart_app', source: 'restart', fallback: 'Gracefully restart mouaif from the current chat.', spec: (m) => m.SPEC && m.SPEC.function });
|
|
306
|
+
try {
|
|
307
|
+
|
|
308
|
+
const ft = require('./tools/files.js');
|
|
309
|
+
for (const name of ft.FILE_TOOL_NAMES) {
|
|
310
|
+
const spec = ft.SPECS && ft.SPECS[name];
|
|
311
|
+
tools.push({
|
|
312
|
+
name,
|
|
313
|
+
kind: 'native',
|
|
314
|
+
source: 'files',
|
|
315
|
+
description: (spec && spec.function && spec.function.description) || '',
|
|
316
|
+
parameters: (spec && spec.function && spec.function.parameters) || null
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} catch { /* files module unavailable; omit */ }
|
|
320
|
+
try {
|
|
321
|
+
const mcpMod = require('./mcp.js');
|
|
322
|
+
const specs = mcpMod.listComposedToolSpecs(projectDir);
|
|
323
|
+
for (const s of (specs || [])) {
|
|
324
|
+
tools.push({
|
|
325
|
+
name: s.name,
|
|
326
|
+
kind: 'mcp',
|
|
327
|
+
source: s.serverSlug || '',
|
|
328
|
+
serverId: s.serverId || '',
|
|
329
|
+
description: s.description || '',
|
|
330
|
+
parameters: s.parameters || null
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} catch { /* mcp module not loaded; no MCP tools */ }
|
|
334
|
+
return sendJSON(res, 200, { tools });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (urlPath === '/api/tools/shell' && method === 'POST') {
|
|
338
|
+
const body = await readJsonOr400(req, res);
|
|
339
|
+
if (!body) return;
|
|
340
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
341
|
+
const cmd = body && typeof body.cmd === 'string' ? body.cmd : '';
|
|
342
|
+
const shellOverride = body && typeof body.shell === 'string' ? body.shell : '';
|
|
343
|
+
const timeoutMs = body && typeof body.timeoutMs === 'number' ? body.timeoutMs : undefined;
|
|
344
|
+
const chatId = body && typeof body.chatId === 'string' ? body.chatId : '';
|
|
345
|
+
const callId = body && typeof body.callId === 'string' ? body.callId : '';
|
|
346
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
347
|
+
if (!cmd) return sendJSON(res, 400, { error: 'cmd is required' });
|
|
348
|
+
if (!chatId || !callId) return sendJSON(res, 400, { error: 'chatId and callId are required' });
|
|
349
|
+
if (!chats.getChat(projectDir, chatId)) return sendJSON(res, 404, { error: 'Chat not found', chatId });
|
|
350
|
+
|
|
351
|
+
// The authorization gate owns enablement. A legacy `tools.shell.enabled`
|
|
352
|
+
// check used to short-circuit here, but the authorization refactor
|
|
353
|
+
// stores enablement as `mode` (off = disabled, ask/allow/allowlist =
|
|
354
|
+
// enabled) with no `enabled` field — so that stale check always read
|
|
355
|
+
// `false` and every direct `/shell` call failed with ETOOL_DISABLED
|
|
356
|
+
// before ever reaching the prompt. authorize() below rejects `off` mode
|
|
357
|
+
// itself (defense-in-depth), so the pre-check is dropped entirely.
|
|
358
|
+
|
|
359
|
+
let authorization;
|
|
360
|
+
try {
|
|
361
|
+
authorization = await require('./tools/authorization.js').authorize({
|
|
362
|
+
projectDir, chatId, callId, tool: 'shell', cmd, summary: cmd, timeoutMs, flow: 'retry'
|
|
363
|
+
});
|
|
364
|
+
} catch (e) {
|
|
365
|
+
const status = e.code === 'ETOOL_DISABLED' || e.code === 'EDENIED' ? 403 : 400;
|
|
366
|
+
return sendJSON(res, status, { ok: false, error: e.message, code: e.code || 'EAUTH' });
|
|
367
|
+
}
|
|
368
|
+
if (authorization.decision === 'prompt') {
|
|
369
|
+
return sendJSON(res, 409, {
|
|
370
|
+
ok: false,
|
|
371
|
+
code: 'EAUTH_REQUIRED',
|
|
372
|
+
chatId,
|
|
373
|
+
callId,
|
|
374
|
+
tool: 'shell',
|
|
375
|
+
cmd,
|
|
376
|
+
timeoutMs: authorization.timeoutMs,
|
|
377
|
+
projectDir
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Streamed NDJSON response (Content-Type: application/x-ndjson):
|
|
382
|
+
// one line per live output chunk while the command is still running,
|
|
383
|
+
// then a final `result` line with the complete tool result. The chat
|
|
384
|
+
// composer's /shell card renders the output lines into its live
|
|
385
|
+
// preview, so a direct run behaves exactly like a model-driven run
|
|
386
|
+
// (which streams `shell_output` SSE frames). Error frames never
|
|
387
|
+
// appear here — the pre-run failures above (authorization, bad
|
|
388
|
+
// input) are plain JSON responses; only the run itself streams.
|
|
389
|
+
// The client treats a non-NDJSON body as a fallback and reads it as
|
|
390
|
+
// JSON, so an older server still works.
|
|
391
|
+
res.writeHead(200, {
|
|
392
|
+
'Content-Type': 'application/x-ndjson; charset=utf-8',
|
|
393
|
+
'Cache-Control': 'no-cache',
|
|
394
|
+
'X-Content-Type-Options': 'nosniff'
|
|
395
|
+
});
|
|
396
|
+
const out = await shellTool.runShell({
|
|
397
|
+
projectDir, cmd,
|
|
398
|
+
shell: shellOverride || undefined,
|
|
399
|
+
timeoutMs: authorization.timeoutMs,
|
|
400
|
+
// Live output deltas for the composer card, mirroring the
|
|
401
|
+
// `shell_output` SSE event of the model-driven path.
|
|
402
|
+
onOutput: (stream, delta) => {
|
|
403
|
+
try {
|
|
404
|
+
res.write(JSON.stringify({ type: 'output', stream, delta }) + '\n');
|
|
405
|
+
} catch { /* client disconnected; the run keeps going server-side */ }
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// Same identity header the model-facing tool message carries, so a
|
|
409
|
+
// composer /shell run reads exactly like a model-driven run.
|
|
410
|
+
if (out && typeof out === 'object' && !out.identity) out.identity = 'mouaif shell';
|
|
411
|
+
try {
|
|
412
|
+
res.end(JSON.stringify({ type: 'result', result: out }) + '\n');
|
|
413
|
+
} catch { /* client gone */ }
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// GET /api/tools/cli/session?projectDir=<abs>
|
|
418
|
+
// Start (or reuse) the persistent interactive terminal session for the
|
|
419
|
+
// project and return its id. The browser then opens GET /events and
|
|
420
|
+
// listens for `cli_output` frames tagged with that id.
|
|
421
|
+
if (urlPath === '/api/tools/cli/session' && method === 'GET') {
|
|
422
|
+
const projectDir = qs(q, 'projectDir');
|
|
423
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
424
|
+
const fs = require('node:fs');
|
|
425
|
+
let real;
|
|
426
|
+
try { real = fs.realpathSync(projectDir); } catch { return sendJSON(res, 400, { error: 'project directory not found', code: 'ENOENT' }); }
|
|
427
|
+
let st;
|
|
428
|
+
try { st = fs.statSync(real); } catch { return sendJSON(res, 400, { error: 'project directory not found', code: 'ENOENT' }); }
|
|
429
|
+
if (!st.isDirectory()) return sendJSON(res, 400, { error: 'projectDir is not a directory', code: 'ENOTDIR' });
|
|
430
|
+
const session = ensureCliSession(real);
|
|
431
|
+
// Attach the SSE output stream to the newly created (or reused)
|
|
432
|
+
// session. Re-used sessions already have their stream attached —
|
|
433
|
+
// attaching twice would double every output frame in the browser.
|
|
434
|
+
if (!session._streamAttached) {
|
|
435
|
+
session._streamAttached = true;
|
|
436
|
+
attachCliStream(session, broadcast);
|
|
437
|
+
}
|
|
438
|
+
const meta = cliShellMeta();
|
|
439
|
+
return sendJSON(res, 200, {
|
|
440
|
+
id: session.id,
|
|
441
|
+
projectDir: real,
|
|
442
|
+
shell: meta.label,
|
|
443
|
+
// True when the session runs on a pseudo-terminal, so prompting
|
|
444
|
+
// programs (npm under 2FA, git, sudo) can ask a question and read
|
|
445
|
+
// the answer. The modal surfaces this so a user knows interactive
|
|
446
|
+
// input is supported.
|
|
447
|
+
interactive: !!session.pty,
|
|
448
|
+
startedAt: session.startedAt,
|
|
449
|
+
defaultDir: real
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// POST /api/tools/cli/command body: { projectDir, cmd }
|
|
454
|
+
// Write one command to the persistent session's stdin. The session
|
|
455
|
+
// stays open; the next command appends after it.
|
|
456
|
+
if (urlPath === '/api/tools/cli/command' && method === 'POST') {
|
|
457
|
+
const body = await readJsonOr400(req, res);
|
|
458
|
+
if (!body) return;
|
|
459
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
460
|
+
const cmd = body && typeof body.cmd === 'string' ? body.cmd : '';
|
|
461
|
+
// `raw` sends the text with no line terminator (a single-key answer).
|
|
462
|
+
const raw = !!(body && body.raw);
|
|
463
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
464
|
+
const session = cliSessions.get(String(projectDir));
|
|
465
|
+
if (!session) return sendJSON(res, 404, { error: 'cli session not found — reopen the command prompt', code: 'ENOSESSION' });
|
|
466
|
+
try {
|
|
467
|
+
if (!writeCliCommand(session, cmd, raw)) {
|
|
468
|
+
return sendJSON(res, 410, { ok: false, error: 'cli session is not accepting input — reopen the command prompt', code: 'ENOWRITE' });
|
|
469
|
+
}
|
|
470
|
+
return sendJSON(res, 200, { ok: true });
|
|
471
|
+
} catch (e) {
|
|
472
|
+
return sendJSON(res, 500, { ok: false, error: e.message });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// POST /api/tools/cli/close body: { projectDir }
|
|
477
|
+
// Kill the persistent session (idempotent).
|
|
478
|
+
if (urlPath === '/api/tools/cli/close' && method === 'POST') {
|
|
479
|
+
const body = await readJsonOr400(req, res);
|
|
480
|
+
if (!body) return;
|
|
481
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
482
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
483
|
+
closeCliSession(projectDir);
|
|
484
|
+
return sendJSON(res, 200, { ok: true });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// POST /api/tools/webpreview body: { projectDir, chatId, url, viewport? }
|
|
488
|
+
// Direct user-facing refresh of the web preview from the full-screen
|
|
489
|
+
// viewer (and the dock's Dismiss/Refresh cycle). The user may change the
|
|
490
|
+
// capture resolution with `viewport` (preset id or "WIDTHxHEIGHT"). The
|
|
491
|
+
// run goes through the same webpreview authorization gate as the
|
|
492
|
+
// model-driven path, so a disabled tool (mode 'off') is refused and an
|
|
493
|
+
// 'ask'-gated project still prompts. The result is returned as plain JSON
|
|
494
|
+
// (no SSE stream) — the chat UI publishes the fresh screenshot into its
|
|
495
|
+
// preview dock exactly as a model-driven capture does.
|
|
496
|
+
if (urlPath === '/api/tools/webpreview' && method === 'POST') {
|
|
497
|
+
const body = await readJsonOr400(req, res);
|
|
498
|
+
if (!body) return;
|
|
499
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
500
|
+
const chatId = body && typeof body.chatId === 'string' ? body.chatId : '';
|
|
501
|
+
const url = body && typeof body.url === 'string' ? body.url.trim() : '';
|
|
502
|
+
const viewport = body && typeof body.viewport === 'string' ? body.viewport : '';
|
|
503
|
+
const callId = (body && typeof body.callId === 'string' && body.callId) ||
|
|
504
|
+
('ui_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7));
|
|
505
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
506
|
+
if (!url) return sendJSON(res, 400, { error: 'url is required' });
|
|
507
|
+
if (!chatId) return sendJSON(res, 400, { error: 'chatId is required' });
|
|
508
|
+
if (!chats.getChat(projectDir, chatId)) return sendJSON(res, 404, { error: 'Chat not found', chatId });
|
|
509
|
+
// Gate through the same authorization module the model path uses. The URL
|
|
510
|
+
// is the summary (matching the allowlist by hostname / full URL) and the
|
|
511
|
+
// flow is 'retry' so a prompt resolves as a normal user decision.
|
|
512
|
+
let authorization;
|
|
513
|
+
try {
|
|
514
|
+
authorization = await require('./tools/authorization.js').authorize({
|
|
515
|
+
projectDir, chatId, callId, tool: 'webpreview', url, summary: url, flow: 'retry'
|
|
516
|
+
});
|
|
517
|
+
} catch (e) {
|
|
518
|
+
const status = e.code === 'ETOOL_DISABLED' || e.code === 'EDENIED' ? 403 : 400;
|
|
519
|
+
return sendJSON(res, status, { ok: false, error: e.message, code: e.code || 'EAUTH' });
|
|
520
|
+
}
|
|
521
|
+
if (authorization.decision === 'prompt') {
|
|
522
|
+
return sendJSON(res, 409, {
|
|
523
|
+
ok: false,
|
|
524
|
+
code: 'EAUTH_REQUIRED',
|
|
525
|
+
chatId,
|
|
526
|
+
callId,
|
|
527
|
+
tool: 'webpreview',
|
|
528
|
+
url,
|
|
529
|
+
projectDir
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// Run the native capture. Only the user-facing result is returned; the
|
|
533
|
+
// screenshot is published to the dock by the same tool-publish hook the
|
|
534
|
+
// model path uses.
|
|
535
|
+
let wp;
|
|
536
|
+
try { wp = require('./tools/webpreview.js'); }
|
|
537
|
+
catch (e) {
|
|
538
|
+
return sendJSON(res, 500, { ok: false, error: 'webpreview tool module unavailable: ' + (e.message || e), code: 'EMODULE' });
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const out = await wp.runWebpreview({ url, viewport: viewport || undefined });
|
|
542
|
+
return sendJSON(res, 200, out);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
return sendJSON(res, 500, { ok: false, error: e.message || String(e), code: e.code || 'EWEBPREVIEW' });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// POST /api/tools/subagent body: { projectDir, chatId, task, agent?, context?, modelId?, providerId? }
|
|
548
|
+
// Direct subagent dispatch from the composer (@agent <task>). Runs the
|
|
549
|
+
// native subagent tool through the same authorization gate and the
|
|
550
|
+
// same dispatcher the model-driven loop uses — one tool_call +
|
|
551
|
+
// tool_result pair, returned in the JSON body (no SSE stream).
|
|
552
|
+
if (urlPath === '/api/tools/subagent' && method === 'POST') {
|
|
553
|
+
const body = await readJsonOr400(req, res);
|
|
554
|
+
if (!body) return;
|
|
555
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
556
|
+
const chatId = body && typeof body.chatId === 'string' ? body.chatId : '';
|
|
557
|
+
const task = body && typeof body.task === 'string' ? body.task.trim() : '';
|
|
558
|
+
const agentName = body && typeof body.agent === 'string' ? body.agent.trim() : '';
|
|
559
|
+
const context = body && typeof body.context === 'string' ? body.context : '';
|
|
560
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
561
|
+
if (!chatId) return sendJSON(res, 400, { error: 'chatId is required' });
|
|
562
|
+
if (!task) return sendJSON(res, 400, { error: 'task is required' });
|
|
563
|
+
const chat = chats.getChat(projectDir, chatId);
|
|
564
|
+
if (!chat) return sendJSON(res, 404, { error: 'Chat not found', chatId });
|
|
565
|
+
|
|
566
|
+
// Model: explicit body.modelId wins, else the chat's current model.
|
|
567
|
+
const modelId = typeof body.modelId === 'string' && body.modelId ? body.modelId : (chat.modelId || '');
|
|
568
|
+
const providerId = typeof body.providerId === 'string' && body.providerId ? body.providerId : (chat.providerId || '');
|
|
569
|
+
if (!modelId) return sendJSON(res, 400, { error: 'No model selected for this chat' });
|
|
570
|
+
let model;
|
|
571
|
+
try { model = resolveModel(modelId, projectDir, providerId); }
|
|
572
|
+
catch (e) { return sendJSON(res, e.code === 'EMODEL_NOT_FOUND' || e.code === 'EPROVIDER_NOT_FOUND' ? 404 : 400, { error: e.message, code: e.code || 'EBADMODEL' }); }
|
|
573
|
+
// Inherit the chat's thinking level so a delegated run (agent pin or
|
|
574
|
+
// generic) matches what a normal chat turn would send. An agent's own
|
|
575
|
+
// thinkingLevel still wins inside the dispatcher.
|
|
576
|
+
if (typeof chat.thinkingLevel === 'string' && chat.thinkingLevel) model.thinkingLevel = chat.thinkingLevel;
|
|
577
|
+
|
|
578
|
+
let appSettings;
|
|
579
|
+
try { appSettings = settings.getApp(); } catch { /* defaults apply */ }
|
|
580
|
+
const args = { task };
|
|
581
|
+
if (agentName) args.agent = agentName;
|
|
582
|
+
if (context) args.context = context;
|
|
583
|
+
|
|
584
|
+
const callId = 'direct_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
585
|
+
const events = [];
|
|
586
|
+
try {
|
|
587
|
+
const out = await ai.runSingleToolCall(
|
|
588
|
+
{ id: callId, name: 'subagent', arguments: JSON.stringify(args) },
|
|
589
|
+
{
|
|
590
|
+
opts: {
|
|
591
|
+
projectDir,
|
|
592
|
+
chatId,
|
|
593
|
+
appSettings,
|
|
594
|
+
promptSize: chat.promptSize,
|
|
595
|
+
enabledTools: Array.isArray(chat.tools) ? chat.tools : null,
|
|
596
|
+
model
|
|
597
|
+
},
|
|
598
|
+
onEvent: (name, data) => events.push({ name, data }),
|
|
599
|
+
convo: null,
|
|
600
|
+
toolSpecs: [],
|
|
601
|
+
promptProfilesMod: null,
|
|
602
|
+
discoveredToolNames: null,
|
|
603
|
+
modelContentForTool: (name, exec) => (exec && exec.content) || '',
|
|
604
|
+
getLastToolCallKey: () => null,
|
|
605
|
+
setLastToolCallKey: () => {},
|
|
606
|
+
getRepeatedToolCallCount: () => 0,
|
|
607
|
+
setRepeatedToolCallCount: () => {},
|
|
608
|
+
REPEATED_TOOL_CALL_LIMIT: 3
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
const exec = out && out.exec;
|
|
612
|
+
const result = exec && exec.result;
|
|
613
|
+
const toolCall = events.find((e) => e.name === 'tool_call');
|
|
614
|
+
if (exec && exec.ok && result && typeof result.text === 'string' && result.text) {
|
|
615
|
+
let total = Number(result.totalCost);
|
|
616
|
+
if (!(isFinite(total) && total >= 0) && result.model && result.usage) {
|
|
617
|
+
try {
|
|
618
|
+
const estimated = usage.computeCost({ model: result.model, usage: result.usage, app: appSettings });
|
|
619
|
+
total = estimated.known ? estimated.total : NaN;
|
|
620
|
+
} catch { total = NaN; }
|
|
621
|
+
}
|
|
622
|
+
const cost = isFinite(total) && total >= 0
|
|
623
|
+
? { known: true, input: 0, output: 0, total, currency: 'USD' }
|
|
624
|
+
: { known: false, input: 0, output: 0, total: 0, currency: 'USD' };
|
|
625
|
+
try {
|
|
626
|
+
messages.appendMessage(projectDir, chatId, {
|
|
627
|
+
role: 'assistant',
|
|
628
|
+
content: result.text,
|
|
629
|
+
usage: result.usage || undefined,
|
|
630
|
+
cost,
|
|
631
|
+
modelId: result.model && result.model.id ? result.model.id : model.id
|
|
632
|
+
});
|
|
633
|
+
} catch { /* direct dispatch still returns its result if persistence fails */ }
|
|
634
|
+
result.cost = cost;
|
|
635
|
+
}
|
|
636
|
+
const payload = {
|
|
637
|
+
ok: !!(exec && exec.ok),
|
|
638
|
+
id: callId,
|
|
639
|
+
name: 'subagent',
|
|
640
|
+
args,
|
|
641
|
+
result,
|
|
642
|
+
toolCall: toolCall ? { id: toolCall.data && toolCall.data.id, name: toolCall.data && toolCall.data.name, args: toolCall.data && toolCall.data.args } : { id: callId, name: 'subagent', args }
|
|
643
|
+
};
|
|
644
|
+
return sendJSON(res, 200, payload);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
const status = e && (e.code === 'ETOOL_DISABLED' || e.code === 'EDENIED') ? 403 : 500;
|
|
647
|
+
return sendJSON(res, status, { ok: false, error: (e && e.message) || String(e), code: (e && e.code) || 'ESUBAGENT' });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return sendJSON(res, 404, { error: 'Not found' });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
module.exports = { handleTools, ensureCliSession, closeCliSession, writeCliCommand, cliShellMeta, attachCliStream };
|