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,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Projects + file editor + file tagging REST handlers. Extracted from
|
|
4
|
+
// the original single-file http-server.js. Shared helpers live in
|
|
5
|
+
// src/server-shared.js.
|
|
6
|
+
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const {
|
|
10
|
+
sendJSON,
|
|
11
|
+
qs,
|
|
12
|
+
readJsonOr400,
|
|
13
|
+
errCodeToHttpStatus,
|
|
14
|
+
settings,
|
|
15
|
+
projects,
|
|
16
|
+
chats,
|
|
17
|
+
files,
|
|
18
|
+
tags,
|
|
19
|
+
safeDecode
|
|
20
|
+
} = require('./server-shared.js');
|
|
21
|
+
|
|
22
|
+
function projectsErrorStatus(err) {
|
|
23
|
+
return errCodeToHttpStatus(err && err.code, 400);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function handleProjects(req, res, parsed) {
|
|
27
|
+
const urlPath = parsed.pathname;
|
|
28
|
+
const method = req.method;
|
|
29
|
+
const q = parsed.query || {};
|
|
30
|
+
|
|
31
|
+
// File editor (in-app CodeMirror popup). See handleFileEditor for the
|
|
32
|
+
// contract; dispatched here so /api/file, /api/files, and the
|
|
33
|
+
// image-preview /api/file-media route all win over the generic
|
|
34
|
+
// /api/projects routes below.
|
|
35
|
+
if (urlPath === '/api/file' || urlPath === '/api/files' || urlPath === '/api/file-media') {
|
|
36
|
+
return handleFileEditor(req, res, parsed);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// File tagging (docs/decisions.md §15). Routes live under a
|
|
40
|
+
// registered project id: /api/projects/:id/tags[/...]. Delegated to
|
|
41
|
+
// handleTags before the folder-picker / registered-project routes so
|
|
42
|
+
// the more specific path wins.
|
|
43
|
+
if (/^\/api\/projects\/[^/]+\/tags(\/.*)?$/.test(urlPath)) {
|
|
44
|
+
return handleTags(req, res, parsed);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// GET /api/projects?dir=<abs> -> list subdirs
|
|
48
|
+
if (urlPath === '/api/projects' && method === 'GET') {
|
|
49
|
+
const dir = typeof q.dir === 'string' && q.dir ? q.dir : os.homedir();
|
|
50
|
+
try {
|
|
51
|
+
const result = projects.listDir(dir);
|
|
52
|
+
return sendJSON(res, 200, result);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return sendJSON(res, projectsErrorStatus(e), { error: e.message, code: e.code, path: e.path || dir });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// GET /api/projects/registered
|
|
59
|
+
if (urlPath === '/api/projects/registered' && method === 'GET') {
|
|
60
|
+
return sendJSON(res, 200, { projects: projects.listProjects() });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// DELETE /api/projects/registered/:id
|
|
64
|
+
const delMatch = urlPath.match(/^\/api\/projects\/registered\/([A-Za-z0-9_-]+)$/);
|
|
65
|
+
if (delMatch && method === 'DELETE') {
|
|
66
|
+
const ok = projects.removeProject(delMatch[1]);
|
|
67
|
+
if (!ok) return sendJSON(res, 404, { error: 'Not found', id: delMatch[1] });
|
|
68
|
+
return sendJSON(res, 200, { ok: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// PATCH /api/projects/registered/:id body: { name }
|
|
72
|
+
if (delMatch && method === 'PATCH') {
|
|
73
|
+
const body = await readJsonOr400(req, res);
|
|
74
|
+
if (!body) return;
|
|
75
|
+
const name = body && typeof body.name === 'string' ? body.name : '';
|
|
76
|
+
if (!name.trim()) return sendJSON(res, 400, { error: 'name is required' });
|
|
77
|
+
const updated = projects.renameProject(delMatch[1], name);
|
|
78
|
+
if (!updated) return sendJSON(res, 404, { error: 'Not found', id: delMatch[1] });
|
|
79
|
+
return sendJSON(res, 200, { project: updated });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// POST /api/projects body: { action, ... }
|
|
83
|
+
if (urlPath === '/api/projects' && method === 'POST') {
|
|
84
|
+
const body = await readJsonOr400(req, res);
|
|
85
|
+
if (!body) return;
|
|
86
|
+
const action = body && body.action;
|
|
87
|
+
try {
|
|
88
|
+
if (action === 'list') {
|
|
89
|
+
const dir = typeof body.dir === 'string' && body.dir ? body.dir : os.homedir();
|
|
90
|
+
return sendJSON(res, 200, projects.listDir(dir));
|
|
91
|
+
}
|
|
92
|
+
if (action === 'create') {
|
|
93
|
+
if (typeof body.parent !== 'string' || typeof body.name !== 'string' || !body.name) {
|
|
94
|
+
return sendJSON(res, 400, { error: 'parent and name are required' });
|
|
95
|
+
}
|
|
96
|
+
const target = path.join(body.parent, body.name);
|
|
97
|
+
const out = projects.createDir(target);
|
|
98
|
+
return sendJSON(res, 201, { ...out, parent: body.parent, name: body.name });
|
|
99
|
+
}
|
|
100
|
+
if (action === 'register') {
|
|
101
|
+
if (typeof body.dir !== 'string' || !body.dir) {
|
|
102
|
+
return sendJSON(res, 400, { error: 'dir is required' });
|
|
103
|
+
}
|
|
104
|
+
const row = projects.registerProject(body.dir);
|
|
105
|
+
// Opt the project into DB-backed settings before seeding the cost so
|
|
106
|
+
// the working tree is never touched (see project-settings-storage.md).
|
|
107
|
+
// Use the REGISTERED path (projects.js canonicalizes it) rather than the
|
|
108
|
+
// raw request value: a trailing slash or a `..` segment would otherwise
|
|
109
|
+
// key the DB row under a path nothing else looks up, and the opt-in
|
|
110
|
+
// would silently do nothing.
|
|
111
|
+
if (body.dbBacked === true) {
|
|
112
|
+
settings.setDbBacked(row.path, true);
|
|
113
|
+
}
|
|
114
|
+
// Seed persisted chat and project totals once for existing histories.
|
|
115
|
+
try { chats.recomputeProjectTotalCost(body.dir); } catch { /* non-fatal */ }
|
|
116
|
+
return sendJSON(res, 200, { project: row, dbBacked: !!body.dbBacked });
|
|
117
|
+
}
|
|
118
|
+
return sendJSON(res, 400, { error: 'Unknown action', action });
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return sendJSON(res, projectsErrorStatus(e), { error: e.message, code: e.code, path: e.path });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'projects' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- File editor API ---------------------------------------------------
|
|
128
|
+
// Read + write text files inside a registered project folder, for the
|
|
129
|
+
// in-app CodeMirror editor popup. All paths are project-relative (or
|
|
130
|
+
// absolute under the project root). Both the project root and every
|
|
131
|
+
// read/write/list path go through files.resolveSafe, so the same
|
|
132
|
+
// home + MOUAIF_ALLOW_ANY_ROOT guard that protects the rest of the
|
|
133
|
+
// server applies here too.
|
|
134
|
+
//
|
|
135
|
+
// Endpoints:
|
|
136
|
+
// GET /api/files?projectDir=<abs>&dir=<abs> -> list a folder
|
|
137
|
+
// GET /api/file?projectDir=<abs>&path=<abs|rel> -> read a text file
|
|
138
|
+
// PUT /api/file body { projectDir, path, content } -> write a text file
|
|
139
|
+
// GET /api/file-media?projectDir=<abs>&path=<abs|rel> -> read an image as data URL
|
|
140
|
+
//
|
|
141
|
+
// Errors map to typed codes so the UI can render the right message
|
|
142
|
+
// (EBINARY -> "binary file, cannot edit", ETOOLARGE -> "file too big",
|
|
143
|
+
// EOUTSIDE_PROJECT -> 403, ENOTIMAGE -> "file is not an image", etc.).
|
|
144
|
+
|
|
145
|
+
function filesErrorStatus(err) {
|
|
146
|
+
return errCodeToHttpStatus(err && err.code, 400);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleFileEditor(req, res, parsed) {
|
|
150
|
+
const urlPath = parsed.pathname;
|
|
151
|
+
const method = req.method;
|
|
152
|
+
const q = parsed.query || {};
|
|
153
|
+
|
|
154
|
+
// GET /api/files?projectDir=<abs>&dir=<abs> -> list a folder
|
|
155
|
+
if (urlPath === '/api/files' && method === 'GET') {
|
|
156
|
+
const projectDir = qs(q, 'projectDir');
|
|
157
|
+
const dir = qs(q, 'dir');
|
|
158
|
+
try {
|
|
159
|
+
return sendJSON(res, 200, files.listDir(projectDir, dir));
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return sendJSON(res, filesErrorStatus(e), { error: e.message, code: e.code, path: e.path });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// GET /api/file?projectDir=<abs>&path=<abs|rel> -> read a text file
|
|
166
|
+
if (urlPath === '/api/file' && method === 'GET') {
|
|
167
|
+
const projectDir = qs(q, 'projectDir');
|
|
168
|
+
const path = qs(q, 'path');
|
|
169
|
+
try {
|
|
170
|
+
const out = await files.readFile(projectDir, path);
|
|
171
|
+
return sendJSON(res, 200, out);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return sendJSON(res, filesErrorStatus(e), { error: e.message, code: e.code, path: e.path, size: e.size, maxBytes: e.maxBytes });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// PUT /api/file body { projectDir, path, content } -> write a text file
|
|
178
|
+
if (urlPath === '/api/file' && method === 'PUT') {
|
|
179
|
+
const body = await readJsonOr400(req, res);
|
|
180
|
+
if (!body) return;
|
|
181
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
182
|
+
const path = body && typeof body.path === 'string' ? body.path : '';
|
|
183
|
+
const content = body && typeof body.content === 'string' ? body.content : null;
|
|
184
|
+
try {
|
|
185
|
+
const out = await files.writeFile(projectDir, path, content);
|
|
186
|
+
return sendJSON(res, 200, out);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return sendJSON(res, filesErrorStatus(e), { error: e.message, code: e.code, path: e.path, size: e.size, maxBytes: e.maxBytes });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// GET /api/file-media?projectDir=<abs>&path=<abs|rel>
|
|
193
|
+
// Read an image file (or other previewable binary) as a base64
|
|
194
|
+
// data URL. Used by the file editor popup to render an <img>
|
|
195
|
+
// preview for .png/.jpg/.gif/.webp/.svg/.bmp/.ico without needing
|
|
196
|
+
// a separate auth-bearing URL. The data URL is small (<= 1 MiB
|
|
197
|
+
// cap, same as the text read).
|
|
198
|
+
if (urlPath === '/api/file-media' && method === 'GET') {
|
|
199
|
+
const projectDir = qs(q, 'projectDir');
|
|
200
|
+
const path = qs(q, 'path');
|
|
201
|
+
try {
|
|
202
|
+
const out = await files.readMedia(projectDir, path);
|
|
203
|
+
return sendJSON(res, 200, out);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return sendJSON(res, filesErrorStatus(e), { error: e.message, code: e.code, path: e.path, size: e.size, maxBytes: e.maxBytes });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'fileEditor' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- File tagging API ---------------------------------------------------
|
|
212
|
+
// Per-project file tags (docs/decisions.md §15). All routes hang off a
|
|
213
|
+
// registered project id so the UI never has to pass an absolute path:
|
|
214
|
+
// GET /api/projects/:id/tags -> { tags: { ... } }
|
|
215
|
+
// PUT /api/projects/:id/tags body { tags: {...} }
|
|
216
|
+
// POST /api/projects/:id/tags/scan body { exts? }
|
|
217
|
+
// DELETE /api/projects/:id/tags/files/<relPath>
|
|
218
|
+
// The project id resolves to its absolute dir via projects.getProject.
|
|
219
|
+
// Tag CRUD requires a registered project (404 otherwise). Path escapes
|
|
220
|
+
// return 403 EOUTSIDE_PROJECT.
|
|
221
|
+
|
|
222
|
+
function tagsErrorStatus(err) {
|
|
223
|
+
return errCodeToHttpStatus(err && err.code);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleTags(req, res, parsed) {
|
|
227
|
+
const urlPath = parsed.pathname;
|
|
228
|
+
const method = req.method;
|
|
229
|
+
|
|
230
|
+
// Pull the project id out of the path and resolve to an absolute dir.
|
|
231
|
+
const idMatch = urlPath.match(/^\/api\/projects\/([^/]+)\/tags/);
|
|
232
|
+
if (!idMatch) return sendJSON(res, 404, { error: 'Not found', scope: 'tags' });
|
|
233
|
+
const projectId = safeDecode(idMatch[1]);
|
|
234
|
+
const project = projects.getProject(projectId);
|
|
235
|
+
if (!project) return sendJSON(res, 404, { error: 'Project not registered', id: projectId });
|
|
236
|
+
const dir = project.path;
|
|
237
|
+
|
|
238
|
+
const rest = urlPath.slice(idMatch[0].length); // '' | '/scan' | '/files/<rel>'
|
|
239
|
+
|
|
240
|
+
// GET /api/projects/:id/tags
|
|
241
|
+
if (rest === '' && method === 'GET') {
|
|
242
|
+
try {
|
|
243
|
+
return sendJSON(res, 200, { tags: tags.getTags(dir) });
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return sendJSON(res, tagsErrorStatus(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// PUT /api/projects/:id/tags body: { tags: { ... } }
|
|
250
|
+
if (rest === '' && method === 'PUT') {
|
|
251
|
+
const body = await readJsonOr400(req, res);
|
|
252
|
+
if (!body) return;
|
|
253
|
+
const map = body && typeof body.tags === 'object' && body.tags ? body.tags : {};
|
|
254
|
+
try {
|
|
255
|
+
return sendJSON(res, 200, { tags: tags.setTags(dir, map) });
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return sendJSON(res, tagsErrorStatus(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// POST /api/projects/:id/tags/scan body: { exts?: [...] }
|
|
262
|
+
if (rest === '/scan' && method === 'POST') {
|
|
263
|
+
const body = await readJsonOr400(req, res);
|
|
264
|
+
if (!body) return;
|
|
265
|
+
const exts = body && Array.isArray(body.exts) ? body.exts : null;
|
|
266
|
+
try {
|
|
267
|
+
return sendJSON(res, 200, { files: tags.scanFiles(dir, exts) });
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return sendJSON(res, tagsErrorStatus(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// DELETE /api/projects/:id/tags/files/<relPath>
|
|
274
|
+
const fileMatch = rest.match(/^\/files\/(.+)$/);
|
|
275
|
+
if (fileMatch && method === 'DELETE') {
|
|
276
|
+
const relPath = safeDecode(fileMatch[1]);
|
|
277
|
+
try {
|
|
278
|
+
const removed = tags.removeTag(dir, relPath);
|
|
279
|
+
if (!removed) return sendJSON(res, 404, { error: 'Tag entry not found', path: relPath });
|
|
280
|
+
return sendJSON(res, 200, { ok: true, removed: relPath });
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return sendJSON(res, tagsErrorStatus(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'tags' });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = { handleProjects, handleFileEditor, handleTags };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Prompts + agent features + project agents REST handlers. Extracted
|
|
3
|
+
// from the original single-file http-server.js. Shared helpers live in
|
|
4
|
+
// src/server-shared.js.
|
|
5
|
+
const {
|
|
6
|
+
sendJSON,
|
|
7
|
+
qs,
|
|
8
|
+
readJsonOr400,
|
|
9
|
+
errCodeToHttpStatus,
|
|
10
|
+
settings,
|
|
11
|
+
prompts,
|
|
12
|
+
chats,
|
|
13
|
+
agents,
|
|
14
|
+
agentFeatures,
|
|
15
|
+
safeDecode
|
|
16
|
+
} = require('./server-shared.js');
|
|
17
|
+
|
|
18
|
+
// ---- Prompts API ---------------------------------------------------------
|
|
19
|
+
// Custom prompts. Stored in the app SQLite database or in project.prompts.
|
|
20
|
+
// Routes:
|
|
21
|
+
// GET /api/prompts[?projectDir=<abs>][&scope=app|project] -> { prompts }
|
|
22
|
+
// GET /api/prompts/:id[?projectDir=<abs>][&scope=app|project] -> { prompt }
|
|
23
|
+
// POST /api/prompts body: { projectDir?, scope?, title?, content, role? } -> { prompt }
|
|
24
|
+
// PATCH /api/prompts/:id body: { projectDir?, scope?, title?, content?, role? } -> { prompt }
|
|
25
|
+
// DELETE /api/prompts/:id[?projectDir=<abs>][&scope=app|project] -> { ok: true }
|
|
26
|
+
async function handlePrompts(req, res, parsed) {
|
|
27
|
+
const urlPath = parsed.pathname;
|
|
28
|
+
const method = req.method;
|
|
29
|
+
const q = parsed.query || {};
|
|
30
|
+
|
|
31
|
+
function projectDirFrom(body) {
|
|
32
|
+
const fromQuery = qs(q, 'projectDir');
|
|
33
|
+
const fromBody = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
34
|
+
return fromQuery || fromBody;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function promptError(e) {
|
|
38
|
+
return errCodeToHttpStatus(e && e.code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GET /api/prompts[?projectDir=<abs>]
|
|
42
|
+
if (urlPath === '/api/prompts' && method === 'GET') {
|
|
43
|
+
const dir = qs(q, 'projectDir');
|
|
44
|
+
const scope = qs(q, 'scope');
|
|
45
|
+
try {
|
|
46
|
+
return sendJSON(res, 200, { prompts: prompts.listPrompts(dir, { scope }) });
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return sendJSON(res, promptError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// GET /api/prompts/:id[?projectDir=<abs>]
|
|
53
|
+
const getMatch = urlPath.match(/^\/api\/prompts\/([^/]+)$/);
|
|
54
|
+
if (getMatch && method === 'GET') {
|
|
55
|
+
const id = safeDecode(getMatch[1]);
|
|
56
|
+
const dir = qs(q, 'projectDir');
|
|
57
|
+
const scope = qs(q, 'scope');
|
|
58
|
+
try {
|
|
59
|
+
const p = prompts.getPrompt(dir, id, { scope });
|
|
60
|
+
if (!p) return sendJSON(res, 404, { error: 'Prompt not found', id });
|
|
61
|
+
return sendJSON(res, 200, { prompt: p });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return sendJSON(res, promptError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// POST /api/prompts body: { projectDir?, scope?, title?, content, role? }
|
|
68
|
+
if (urlPath === '/api/prompts' && method === 'POST') {
|
|
69
|
+
const body = await readJsonOr400(req, res);
|
|
70
|
+
if (!body) return;
|
|
71
|
+
const dir = projectDirFrom(body);
|
|
72
|
+
const scope = body.scope || (dir ? 'project' : 'app');
|
|
73
|
+
if (!dir && scope === 'project') {
|
|
74
|
+
return sendJSON(res, 400, { error: 'projectDir is required for project-scoped prompts' });
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const p = prompts.createPrompt(dir, body || {});
|
|
78
|
+
return sendJSON(res, 201, { prompt: p });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return sendJSON(res, promptError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// PATCH /api/prompts/:id body: { projectDir?, scope?, title?, content?, role? }
|
|
85
|
+
if (getMatch && method === 'PATCH') {
|
|
86
|
+
const id = safeDecode(getMatch[1]);
|
|
87
|
+
const body = await readJsonOr400(req, res);
|
|
88
|
+
if (!body) return;
|
|
89
|
+
const dir = projectDirFrom(body);
|
|
90
|
+
try {
|
|
91
|
+
const p = prompts.updatePrompt(dir, id, body || {});
|
|
92
|
+
if (!p) return sendJSON(res, 404, { error: 'Prompt not found', id });
|
|
93
|
+
return sendJSON(res, 200, { prompt: p });
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return sendJSON(res, promptError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// DELETE /api/prompts/:id[?projectDir=<abs>]
|
|
100
|
+
if (getMatch && method === 'DELETE') {
|
|
101
|
+
const id = safeDecode(getMatch[1]);
|
|
102
|
+
const dir = qs(q, 'projectDir');
|
|
103
|
+
const scope = qs(q, 'scope');
|
|
104
|
+
try {
|
|
105
|
+
let clearedChats = 0;
|
|
106
|
+
const removed = prompts.deletePrompt(dir, id, {
|
|
107
|
+
scope,
|
|
108
|
+
onRemoved: (deletedId) => {
|
|
109
|
+
if (dir) {
|
|
110
|
+
clearedChats = chats.clearPromptId(dir, deletedId);
|
|
111
|
+
} else {
|
|
112
|
+
try {
|
|
113
|
+
const allProjects = require('./projects.js').listProjects();
|
|
114
|
+
for (const prj of allProjects) {
|
|
115
|
+
if (prj && prj.path) {
|
|
116
|
+
clearedChats += chats.clearPromptId(prj.path, deletedId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch { /* non-fatal */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (!removed) return sendJSON(res, 404, { error: 'Prompt not found', id });
|
|
124
|
+
return sendJSON(res, 200, { ok: true, removed: id, clearedChats });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return sendJSON(res, promptError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'prompts' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- Agent features API ------------------------------------------------
|
|
134
|
+
// Structured state of every mouaif feature for a project. Returns the
|
|
135
|
+
// same data the `list_features` tool provides, without requiring a chat.
|
|
136
|
+
// Routes:
|
|
137
|
+
// GET /api/features?projectDir=<abs> -> { features: { ... } }
|
|
138
|
+
async function handleFeatures(req, res, parsed) {
|
|
139
|
+
const dir = (parsed.query && parsed.query.projectDir || '').trim();
|
|
140
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
141
|
+
try {
|
|
142
|
+
const project = require('./settings.js').getProject(dir);
|
|
143
|
+
let authz = null;
|
|
144
|
+
try { authz = require('./tools/authorization.js').getAuthorization(dir); } catch { /* safe default */ }
|
|
145
|
+
let mcpServers = null;
|
|
146
|
+
try { mcpServers = require('./mcp.js').listServers(dir); } catch { /* safe default */ }
|
|
147
|
+
const featureContent = agentFeatures.buildFeatureSummary({ projectDir: dir, project, authz, mcpServers });
|
|
148
|
+
const jsonState = await agentFeatures.dispatchListFeatures({}, { projectDir: dir });
|
|
149
|
+
sendJSON(res, 200, {
|
|
150
|
+
features: (jsonState && jsonState.result) || {},
|
|
151
|
+
summary: featureContent
|
|
152
|
+
});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
sendJSON(res, 500, { error: e.message, code: 'EINTERNAL' });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- Project agents API -----------------------------------------------
|
|
159
|
+
// Project-scoped named personas stored in .mouaif.json under `agents`.
|
|
160
|
+
// Each agent = { name, content, tools?, modelId? } — a delegation target
|
|
161
|
+
// for the native `subagent` tool, nothing else. See docs/features/agents.md.
|
|
162
|
+
// Routes:
|
|
163
|
+
// GET /api/agents?projectDir=<abs> -> { agents }
|
|
164
|
+
// POST /api/agents body: { projectDir, name, content, tools?, modelId? }
|
|
165
|
+
// GET /api/agents/:name?projectDir=<abs> -> { agent } | 404
|
|
166
|
+
// PATCH /api/agents/:name body: { projectDir, name?, content?, tools?, modelId? }
|
|
167
|
+
// DELETE /api/agents/:name?projectDir=<abs>
|
|
168
|
+
async function handleAgents(req, res, parsed) {
|
|
169
|
+
const urlPath = parsed.pathname;
|
|
170
|
+
const method = req.method;
|
|
171
|
+
const q = parsed.query || {};
|
|
172
|
+
|
|
173
|
+
function agentDirFrom(body) {
|
|
174
|
+
const fromQuery = qs(q, 'projectDir');
|
|
175
|
+
const fromBody = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
176
|
+
return fromQuery || fromBody;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function agentError(e) {
|
|
180
|
+
return errCodeToHttpStatus(e && e.code);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (method !== 'GET' && method !== 'POST' && method !== 'PATCH' && method !== 'DELETE') {
|
|
184
|
+
return sendJSON(res, 405, { error: 'Method not allowed' });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// GET/DELETE carry projectDir in the query string; POST/PATCH carry
|
|
188
|
+
// it in the JSON body. Only the query-string routes can 400 up front.
|
|
189
|
+
const dir = qs(q, 'projectDir');
|
|
190
|
+
|
|
191
|
+
// GET /api/agents?projectDir=...
|
|
192
|
+
if (urlPath === '/api/agents' && method === 'GET') {
|
|
193
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
194
|
+
try {
|
|
195
|
+
return sendJSON(res, 200, { agents: agents.list(dir) });
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return sendJSON(res, agentError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// POST /api/agents body: { projectDir, name, content, tools? }
|
|
202
|
+
if (urlPath === '/api/agents' && method === 'POST') {
|
|
203
|
+
const body = await readJsonOr400(req, res);
|
|
204
|
+
if (!body) return;
|
|
205
|
+
const projectDir = agentDirFrom(body);
|
|
206
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
207
|
+
try {
|
|
208
|
+
const agent = agents.create(projectDir, body || {});
|
|
209
|
+
return sendJSON(res, 201, { agent });
|
|
210
|
+
} catch (e) {
|
|
211
|
+
const status = e.code === 'EBADINPUT' ? 400 : 500;
|
|
212
|
+
return sendJSON(res, status, { error: e.message, code: e.code || 'INTERNAL' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// GET|PATCH|DELETE /api/agents/:name
|
|
217
|
+
let getMatch = urlPath.match(/^\/api\/agents\/([^/]+)$/);
|
|
218
|
+
if (getMatch) {
|
|
219
|
+
const name = safeDecode(getMatch[1]);
|
|
220
|
+
if (method === 'GET') {
|
|
221
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
222
|
+
try {
|
|
223
|
+
const agent = agents.get(dir, name);
|
|
224
|
+
if (!agent) return sendJSON(res, 404, { error: 'Agent not found', name });
|
|
225
|
+
return sendJSON(res, 200, { agent });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
return sendJSON(res, agentError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (method === 'PATCH') {
|
|
231
|
+
const body = await readJsonOr400(req, res);
|
|
232
|
+
if (!body) return;
|
|
233
|
+
const patchDir = dir || (body && typeof body.projectDir === 'string' ? body.projectDir : '');
|
|
234
|
+
if (!patchDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
235
|
+
try {
|
|
236
|
+
const agent = agents.update(patchDir, name, body || {});
|
|
237
|
+
if (!agent) return sendJSON(res, 404, { error: 'Agent not found', name });
|
|
238
|
+
return sendJSON(res, 200, { agent });
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return sendJSON(res, agentError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (method === 'DELETE') {
|
|
244
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
245
|
+
try {
|
|
246
|
+
const ok = agents.remove(dir, name);
|
|
247
|
+
if (!ok) return sendJSON(res, 404, { error: 'Agent not found', name });
|
|
248
|
+
return sendJSON(res, 200, { ok: true, removed: name });
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return sendJSON(res, agentError(e), { error: e.message, code: e.code || 'INTERNAL' });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return sendJSON(res, 405, { error: 'Method not allowed' });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'agents' });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { handlePrompts, handleFeatures, handleAgents };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Browser push notification REST handlers. Extracted from the original
|
|
4
|
+
// single-file http-server.js. Shared helpers live in src/server-shared.js.
|
|
5
|
+
|
|
6
|
+
const { sendJSON, readJsonOr400, push } = require('./server-shared.js');
|
|
7
|
+
|
|
8
|
+
async function handlePush(req, res, parsed, sessionToken, servedOrigin) {
|
|
9
|
+
const urlPath = parsed.pathname;
|
|
10
|
+
const method = req.method;
|
|
11
|
+
const sid = push.sessionIdFromToken(sessionToken);
|
|
12
|
+
|
|
13
|
+
// GET /api/push/vapid-public-key
|
|
14
|
+
if (urlPath === '/api/push/vapid-public-key' && method === 'GET') {
|
|
15
|
+
const publicKey = push.getVapidPublicKey();
|
|
16
|
+
if (!publicKey) return sendJSON(res, 500, { error: 'VAPID keys not initialised', code: 'ENOVAPID' });
|
|
17
|
+
return sendJSON(res, 200, { publicKey });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// GET /api/push/config — safe notification configuration. Opening the
|
|
21
|
+
// settings screen also repairs a missing VAPID pair. The private key never
|
|
22
|
+
// leaves the server.
|
|
23
|
+
if (urlPath === '/api/push/config' && method === 'GET') {
|
|
24
|
+
try {
|
|
25
|
+
return sendJSON(res, 200, push.getPushConfig(servedOrigin));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return sendJSON(res, 500, { error: e.message, code: 'EVAPID' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POST /api/push/subscribe
|
|
32
|
+
if (urlPath === '/api/push/subscribe' && method === 'POST') {
|
|
33
|
+
if (!sid) return sendJSON(res, 401, { error: 'No session', code: 'ESESSION' });
|
|
34
|
+
const body = await readJsonOr400(req, res);
|
|
35
|
+
if (!body) return;
|
|
36
|
+
if (!body.subscription || !body.subscription.endpoint) {
|
|
37
|
+
return sendJSON(res, 400, { error: 'subscription with endpoint is required', code: 'EBADINPUT' });
|
|
38
|
+
}
|
|
39
|
+
const { endpoint, keys } = body.subscription;
|
|
40
|
+
if (!keys || !keys.p256dh || !keys.auth) {
|
|
41
|
+
return sendJSON(res, 400, { error: 'subscription must include keys.p256dh and keys.auth', code: 'EBADINPUT' });
|
|
42
|
+
}
|
|
43
|
+
// How this device presents a notification. The status bar is sized from
|
|
44
|
+
// these facts (src/statusBar.js): `chars` is the measured body line,
|
|
45
|
+
// `viewportWidth` keeps a measurement honest and scales the fallback,
|
|
46
|
+
// and `os` + `osVersion` pick how many lines the platform previews and
|
|
47
|
+
// the platform's own fallback width. `style` is 'collapsed' or
|
|
48
|
+
// 'expanded' so a one-line presentation puts the bar and message on one
|
|
49
|
+
// row. All optional; an omitted field keeps whatever this device
|
|
50
|
+
// reported last. `statusBarMaxChars` is the older, bare-count shape and
|
|
51
|
+
// stays supported.
|
|
52
|
+
const statusBarMaxChars = Number(body.subscription.statusBarMaxChars);
|
|
53
|
+
const statusBarProfile = body.subscription.statusBarProfile;
|
|
54
|
+
try {
|
|
55
|
+
const result = push.addSubscription({
|
|
56
|
+
sessionId: sid,
|
|
57
|
+
endpoint,
|
|
58
|
+
p256dh: keys.p256dh,
|
|
59
|
+
auth: keys.auth,
|
|
60
|
+
origin: servedOrigin || null,
|
|
61
|
+
statusBarMaxChars: Number.isFinite(statusBarMaxChars) && statusBarMaxChars > 0 ? statusBarMaxChars : null,
|
|
62
|
+
statusBarProfile
|
|
63
|
+
});
|
|
64
|
+
return sendJSON(res, 200, { ok: true, id: result.id });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return sendJSON(res, 500, { error: e.message, code: 'EDB' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// DELETE /api/push/subscribe body: { endpoint }
|
|
71
|
+
if (urlPath === '/api/push/subscribe' && method === 'DELETE') {
|
|
72
|
+
if (!sid) return sendJSON(res, 401, { error: 'No session', code: 'ESESSION' });
|
|
73
|
+
const body = await readJsonOr400(req, res);
|
|
74
|
+
if (!body) return;
|
|
75
|
+
const endpoint = body && body.endpoint;
|
|
76
|
+
if (!endpoint) return sendJSON(res, 400, { error: 'endpoint is required', code: 'EBADINPUT' });
|
|
77
|
+
const removed = push.removeSubscription(endpoint, sid);
|
|
78
|
+
return sendJSON(res, 200, { ok: true, removed });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// GET /api/push/subscriptions
|
|
82
|
+
if (urlPath === '/api/push/subscriptions' && method === 'GET') {
|
|
83
|
+
if (!sid) return sendJSON(res, 200, { subscriptions: [] });
|
|
84
|
+
return sendJSON(res, 200, { subscriptions: push.listSubscriptions(sid) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// POST /api/push/test
|
|
88
|
+
if (urlPath === '/api/push/test' && method === 'POST') {
|
|
89
|
+
if (!sid) return sendJSON(res, 401, { error: 'No session', code: 'ESESSION' });
|
|
90
|
+
push.sendPushToSession(sid, {
|
|
91
|
+
title: 'mouaif notifications',
|
|
92
|
+
body: 'Notifications are ready on this browser.',
|
|
93
|
+
tag: 'mouaif-notification-test',
|
|
94
|
+
data: { kind: 'test', url: '/#/settings/notifications' }
|
|
95
|
+
});
|
|
96
|
+
return sendJSON(res, 200, { ok: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'push' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { handlePush };
|