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,406 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Settings REST handlers. Extracted from the original single-file
|
|
4
|
+
// http-server.js so no file stays above ~2 000 lines. See
|
|
5
|
+
// src/server-shared.js for the shared helpers (sendJSON, readJsonBody,
|
|
6
|
+
// settingsForClient, sanitizeClientEntries, ...).
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
sendJSON,
|
|
10
|
+
qs,
|
|
11
|
+
readJsonOr400,
|
|
12
|
+
settingsForClient,
|
|
13
|
+
projectForClient,
|
|
14
|
+
sanitizeClientEntries,
|
|
15
|
+
connectionForClient,
|
|
16
|
+
modelForClient,
|
|
17
|
+
RESETTABLE_APP_KEYS,
|
|
18
|
+
settings,
|
|
19
|
+
ai
|
|
20
|
+
} = require('./server-shared.js');
|
|
21
|
+
|
|
22
|
+
async function handleSettings(req, res, parsed) {
|
|
23
|
+
const urlPath = parsed.pathname;
|
|
24
|
+
const method = req.method;
|
|
25
|
+
const q = parsed.query || {};
|
|
26
|
+
|
|
27
|
+
// GET /api/settings -> { app, defaults, home }
|
|
28
|
+
if (urlPath === '/api/settings' && method === 'GET') {
|
|
29
|
+
return sendJSON(res, 200, {
|
|
30
|
+
home: settings.MOUAIF_HOME,
|
|
31
|
+
defaults: settingsForClient(settings.DEFAULTS),
|
|
32
|
+
app: settingsForClient(settings.getApp())
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// GET /api/settings/resolved?projectDir=<abs path>
|
|
37
|
+
if (urlPath === '/api/settings/resolved' && method === 'GET') {
|
|
38
|
+
const dir = qs(q, 'projectDir');
|
|
39
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
40
|
+
try {
|
|
41
|
+
return sendJSON(res, 200, { resolved: projectForClient(settings.getResolved(dir)) });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const status = e.code === 'MOUAIF_PROJECT_PARSE_ERROR' ? 422 : 500;
|
|
44
|
+
return sendJSON(res, status, { error: e.message, code: e.code || 'INTERNAL' });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GET /api/settings/project?projectDir=<abs path> -> { project, path, dbBacked }
|
|
49
|
+
// Raw project file (no app merge, no defaults). The UI uses this to
|
|
50
|
+
// show the project-level values separately from the resolved view.
|
|
51
|
+
if (urlPath === '/api/settings/project' && method === 'GET') {
|
|
52
|
+
const dir = qs(q, 'projectDir');
|
|
53
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
54
|
+
try {
|
|
55
|
+
const dbBacked = settings.isDbBacked(dir);
|
|
56
|
+
return sendJSON(res, 200, {
|
|
57
|
+
project: projectForClient(settings.getProject(dir)),
|
|
58
|
+
path: dbBacked ? null : settings.getProjectPath(dir),
|
|
59
|
+
dbBacked
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const status = e.code === 'MOUAIF_PROJECT_PARSE_ERROR' ? 422 : 500;
|
|
63
|
+
return sendJSON(res, status, { error: e.message, code: e.code || 'INTERNAL' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PUT /api/settings/app body: { ...patch } (shallow merge into app store)
|
|
68
|
+
if (urlPath === '/api/settings/app' && method === 'PUT') {
|
|
69
|
+
const patch = await readJsonOr400(req, res);
|
|
70
|
+
if (!patch) return;
|
|
71
|
+
try {
|
|
72
|
+
for (const key of ['providers', 'models']) {
|
|
73
|
+
sanitizeClientEntries(patch, key, settings.getApp()[key]);
|
|
74
|
+
}
|
|
75
|
+
const next = settings.setApp(patch);
|
|
76
|
+
return sendJSON(res, 200, { app: settingsForClient(next) });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return sendJSON(res, 400, { error: e.message });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// PUT /api/settings/project body: { projectDir, ...patch }
|
|
83
|
+
if (urlPath === '/api/settings/project' && method === 'PUT') {
|
|
84
|
+
const body = await readJsonOr400(req, res);
|
|
85
|
+
if (!body) return;
|
|
86
|
+
const { projectDir, unset, ...patch } = body || {};
|
|
87
|
+
if (!projectDir || typeof projectDir !== 'string') {
|
|
88
|
+
return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// Same redaction contract as the app store: a round-tripped project
|
|
92
|
+
// snapshot must not persist the response-only `hasApiKey` marker,
|
|
93
|
+
// and an entry re-submitted without `apiKey` keeps the stored one.
|
|
94
|
+
const currentProject = settings.getProject(projectDir);
|
|
95
|
+
for (const key of ['providers', 'models']) {
|
|
96
|
+
sanitizeClientEntries(patch, key, currentProject[key]);
|
|
97
|
+
}
|
|
98
|
+
let next = Object.keys(patch).length ? settings.setProject(projectDir, patch) : currentProject;
|
|
99
|
+
if (Array.isArray(unset) && unset.length) next = settings.unsetProjectKeys(projectDir, unset);
|
|
100
|
+
const dbBacked = settings.isDbBacked(projectDir);
|
|
101
|
+
return sendJSON(res, 200, {
|
|
102
|
+
project: projectForClient(next),
|
|
103
|
+
path: dbBacked ? null : settings.getProjectPath(projectDir),
|
|
104
|
+
dbBacked
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return sendJSON(res, 400, { error: e.message });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// POST /api/settings/app/providers body: { id, baseUrl?, apiKey?, auth?, oauthAccount? }
|
|
112
|
+
// Provider connections are app-level. Project model records reference
|
|
113
|
+
// them by `provider`, keeping credentials out of project files.
|
|
114
|
+
if (urlPath === '/api/settings/app/providers' && method === 'POST') {
|
|
115
|
+
const body = await readJsonOr400(req, res);
|
|
116
|
+
if (!body) return;
|
|
117
|
+
if (!body || typeof body !== 'object' || typeof body.id !== 'string' || !body.id.trim()) {
|
|
118
|
+
return sendJSON(res, 400, { error: 'id is required' });
|
|
119
|
+
}
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(ai.ENDPOINTS, body.id)) {
|
|
121
|
+
return sendJSON(res, 400, { error: 'Unknown provider', id: body.id });
|
|
122
|
+
}
|
|
123
|
+
if (body.auth !== undefined && body.auth !== 'apikey' && body.auth !== 'oauth') {
|
|
124
|
+
return sendJSON(res, 400, { error: 'auth must be "apikey" or "oauth"' });
|
|
125
|
+
}
|
|
126
|
+
if (body.oauthAccount !== undefined && (typeof body.oauthAccount !== 'string' || body.oauthAccount.length > 256)) {
|
|
127
|
+
return sendJSON(res, 400, { error: 'oauthAccount must be a string' });
|
|
128
|
+
}
|
|
129
|
+
const app = settings.getApp();
|
|
130
|
+
const providers = Array.isArray(app.providers) ? app.providers.slice() : [];
|
|
131
|
+
const idx = providers.findIndex(p => p && p.id === body.id);
|
|
132
|
+
const merged = Object.assign({}, idx >= 0 ? providers[idx] : {}, body, { id: body.id.trim() });
|
|
133
|
+
delete merged.hasApiKey;
|
|
134
|
+
if (!Object.prototype.hasOwnProperty.call(body, 'apiKey') && idx >= 0 && providers[idx].apiKey) {
|
|
135
|
+
merged.apiKey = providers[idx].apiKey;
|
|
136
|
+
}
|
|
137
|
+
if (merged.auth === 'oauth') delete merged.apiKey;
|
|
138
|
+
if (idx >= 0) providers[idx] = merged; else providers.push(merged);
|
|
139
|
+
try {
|
|
140
|
+
const next = settings.setApp({ providers });
|
|
141
|
+
return sendJSON(res, 200, {
|
|
142
|
+
provider: connectionForClient(merged),
|
|
143
|
+
providers: next.providers.map(connectionForClient)
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return sendJSON(res, 400, { error: e.message });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// DELETE /api/settings/app/providers/:id
|
|
151
|
+
const delProviderMatch = urlPath.match(/^\/api\/settings\/app\/providers\/([A-Za-z0-9._-]+)$/);
|
|
152
|
+
if (delProviderMatch && method === 'DELETE') {
|
|
153
|
+
const id = delProviderMatch[1];
|
|
154
|
+
const app = settings.getApp();
|
|
155
|
+
const providers = Array.isArray(app.providers) ? app.providers.slice() : [];
|
|
156
|
+
const idx = providers.findIndex(p => p && p.id === id);
|
|
157
|
+
if (idx < 0) return sendJSON(res, 404, { error: 'Provider not found', id });
|
|
158
|
+
const [removed] = providers.splice(idx, 1);
|
|
159
|
+
try {
|
|
160
|
+
settings.setApp({ providers });
|
|
161
|
+
return sendJSON(res, 200, {
|
|
162
|
+
ok: true,
|
|
163
|
+
removed: connectionForClient(removed),
|
|
164
|
+
providers: providers.map(connectionForClient)
|
|
165
|
+
});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return sendJSON(res, 400, { error: e.message });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Legacy compatibility: app-level model CRUD is retained for older
|
|
172
|
+
// clients. New clients configure providers globally and models per project.
|
|
173
|
+
// POST /api/settings/app/models body: { ...model }
|
|
174
|
+
// Adds a model to the app-level models array, or merges into an
|
|
175
|
+
// existing entry with the same id. `id` is required; `provider` is
|
|
176
|
+
// required on add but optional on re-add (the existing entry is the
|
|
177
|
+
// source of truth for that field). Returns the merged model and the
|
|
178
|
+
// updated models array.
|
|
179
|
+
if (urlPath === '/api/settings/app/models' && method === 'POST') {
|
|
180
|
+
const body = await readJsonOr400(req, res);
|
|
181
|
+
if (!body) return;
|
|
182
|
+
if (!body || typeof body !== 'object' || !body.id || typeof body.id !== 'string') {
|
|
183
|
+
return sendJSON(res, 400, { error: 'id is required' });
|
|
184
|
+
}
|
|
185
|
+
// Light validation: auth enum, oauthAccount shape, and a sanity
|
|
186
|
+
// check that OAuth models don't carry a stale apiKey. We don't
|
|
187
|
+
// enforce provider↔auth combos here (e.g. anthropic + oauth is
|
|
188
|
+
// valid, openai-compatible + apikey is valid); that's the AI
|
|
189
|
+
// client's job at request time.
|
|
190
|
+
if (body.auth !== undefined && body.auth !== 'apikey' && body.auth !== 'oauth') {
|
|
191
|
+
return sendJSON(res, 400, { error: 'auth must be "apikey" or "oauth"' });
|
|
192
|
+
}
|
|
193
|
+
if (body.oauthAccount !== undefined && (typeof body.oauthAccount !== 'string' || body.oauthAccount.length > 256)) {
|
|
194
|
+
return sendJSON(res, 400, { error: 'oauthAccount must be a string' });
|
|
195
|
+
}
|
|
196
|
+
if (body.auth === 'oauth' && body.apiKey) {
|
|
197
|
+
// OAuth models must not carry a leftover apiKey from a previous
|
|
198
|
+
// apikey-mode entry. Drop it on merge rather than 400 — the user
|
|
199
|
+
// may have toggled auth modes and forgotten to clear the key.
|
|
200
|
+
delete body.apiKey;
|
|
201
|
+
}
|
|
202
|
+
const app = settings.getApp();
|
|
203
|
+
const models = Array.isArray(app.models) ? app.models.slice() : [];
|
|
204
|
+
const idx = models.findIndex(m => m && m.id === body.id);
|
|
205
|
+
if (idx < 0 && !body.provider) {
|
|
206
|
+
return sendJSON(res, 400, { error: 'provider is required when adding a new model' });
|
|
207
|
+
}
|
|
208
|
+
const merged = Object.assign({}, idx >= 0 ? models[idx] : {}, body);
|
|
209
|
+
// Same dedup for the merged result (an existing apikey record
|
|
210
|
+
// toggled to oauth would still carry apiKey from the prior entry).
|
|
211
|
+
if (merged.auth === 'oauth') delete merged.apiKey;
|
|
212
|
+
if (idx >= 0) models[idx] = merged; else models.push(merged);
|
|
213
|
+
try {
|
|
214
|
+
const next = settings.setApp({ models });
|
|
215
|
+
return sendJSON(res, 200, {
|
|
216
|
+
model: modelForClient(merged),
|
|
217
|
+
models: next.models.map(modelForClient)
|
|
218
|
+
});
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return sendJSON(res, 400, { error: e.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// DELETE /api/settings/app/models/:id -> { ok, removed, models }
|
|
225
|
+
const delModelMatch = urlPath.match(/^\/api\/settings\/app\/models\/([A-Za-z0-9._-]+)$/);
|
|
226
|
+
if (delModelMatch && method === 'DELETE') {
|
|
227
|
+
const id = delModelMatch[1];
|
|
228
|
+
const app = settings.getApp();
|
|
229
|
+
const models = Array.isArray(app.models) ? app.models.slice() : [];
|
|
230
|
+
const idx = models.findIndex(m => m && m.id === id);
|
|
231
|
+
if (idx < 0) return sendJSON(res, 404, { error: 'Model not found', id });
|
|
232
|
+
const [removed] = models.splice(idx, 1);
|
|
233
|
+
try {
|
|
234
|
+
settings.setApp({ models });
|
|
235
|
+
return sendJSON(res, 200, {
|
|
236
|
+
ok: true,
|
|
237
|
+
removed: modelForClient(removed),
|
|
238
|
+
models: models.map(modelForClient)
|
|
239
|
+
});
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return sendJSON(res, 400, { error: e.message });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// GET /api/settings/models/recent?projectDir=<abs>
|
|
246
|
+
// Returns the recent models list for the given project (newest first, capped at 20).
|
|
247
|
+
if (urlPath === '/api/settings/models/recent' && method === 'GET') {
|
|
248
|
+
const dir = qs(q, 'projectDir');
|
|
249
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
250
|
+
return sendJSON(res, 200, { recent: settings.getRecentModels(dir) });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// POST /api/settings/models/recent body: { projectDir, provider, modelId }
|
|
254
|
+
// Records a model as recently used (touches timestamp, deduplicates, caps at 20).
|
|
255
|
+
if (urlPath === '/api/settings/models/recent' && method === 'POST') {
|
|
256
|
+
const body = await readJsonOr400(req, res);
|
|
257
|
+
if (!body) return;
|
|
258
|
+
const { projectDir, provider, modelId } = body || {};
|
|
259
|
+
if (!projectDir || !provider || !modelId) {
|
|
260
|
+
return sendJSON(res, 400, { error: 'projectDir, provider, and modelId are required' });
|
|
261
|
+
}
|
|
262
|
+
if (!settings.canonicalProjectDir(projectDir)) {
|
|
263
|
+
return sendJSON(res, 400, { error: 'projectDir must be an absolute path' });
|
|
264
|
+
}
|
|
265
|
+
settings.touchRecentModel(projectDir, provider, modelId);
|
|
266
|
+
return sendJSON(res, 200, { ok: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// DELETE /api/settings/models/recent?projectDir=<abs>
|
|
270
|
+
// Clears the recent models list for the given project.
|
|
271
|
+
if (urlPath === '/api/settings/models/recent' && method === 'DELETE') {
|
|
272
|
+
const dir = qs(q, 'projectDir');
|
|
273
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
274
|
+
settings.clearRecentModels(dir);
|
|
275
|
+
return sendJSON(res, 200, { ok: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// POST /api/settings/app/reset body: { keys: ['models', 'flags'] }
|
|
279
|
+
// Clears the listed app-level keys, restoring them to defaults.
|
|
280
|
+
// Implementation: build a fresh patch that contains only the keys
|
|
281
|
+
// NOT in the reset list. setApp shallow-merges, so omitting a key
|
|
282
|
+
// from the patch is a no-op — we need a stronger reset, so we
|
|
283
|
+
// explicitly delete the keys from a clone of the current app and
|
|
284
|
+
// write that clone back.
|
|
285
|
+
if (urlPath === '/api/settings/app/reset' && method === 'POST') {
|
|
286
|
+
const body = await readJsonOr400(req, res);
|
|
287
|
+
if (!body) return;
|
|
288
|
+
const keys = Array.isArray(body && body.keys) ? body.keys : [];
|
|
289
|
+
const bad = keys.filter(k => !RESETTABLE_APP_KEYS.has(k));
|
|
290
|
+
if (bad.length) return sendJSON(res, 400, { error: 'Unknown key(s)', bad });
|
|
291
|
+
// Build a fresh object that omits the reset keys, then write it
|
|
292
|
+
// through. SQLite stores the new object verbatim (setApp uses
|
|
293
|
+
// { ...current, ...patch } internally; here we pass a patch that
|
|
294
|
+
// does NOT include the reset keys, but the merged result still
|
|
295
|
+
// contains them because the stored `current` does). So we need a
|
|
296
|
+
// explicit delete: a special-cased "replace" path. The simplest
|
|
297
|
+
// correct approach is to use the app store's underlying SQL: an
|
|
298
|
+
// UPSERT of an object that has the keys removed.
|
|
299
|
+
const current = settings.getApp();
|
|
300
|
+
const next = Object.assign({}, current);
|
|
301
|
+
for (const k of keys) delete next[k];
|
|
302
|
+
try {
|
|
303
|
+
// setApp is shallow-merge; passing the trimmed object and using
|
|
304
|
+
// a direct write would be ideal, but we don't expose a
|
|
305
|
+
// replace-at-the-root method. Cheapest correct option: pass an
|
|
306
|
+
// empty patch alongside a sentinel, OR just deep-copy the app
|
|
307
|
+
// and write the trimmed version through a new method.
|
|
308
|
+
// -> we add setAppReplace for this.
|
|
309
|
+
const result = settings.setAppReplace(next);
|
|
310
|
+
return sendJSON(res, 200, { app: settingsForClient(result), reset: keys });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
return sendJSON(res, 400, { error: e.message });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// GET /api/settings/project/storage?projectDir=<abs>
|
|
317
|
+
// Reports where this project's settings live. `dbBacked: true` means the
|
|
318
|
+
// settings are persisted in the app SQLite store (no .mouaif.json written);
|
|
319
|
+
// `dbBacked: false` means the .mouaif.json file on disk.
|
|
320
|
+
if (urlPath === '/api/settings/project/storage' && method === 'GET') {
|
|
321
|
+
const dir = qs(q, 'projectDir');
|
|
322
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
323
|
+
try {
|
|
324
|
+
return sendJSON(res, 200, { dbBacked: settings.isDbBacked(dir) });
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return sendJSON(res, 500, { error: e.message, code: e.code || 'INTERNAL' });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// PUT /api/settings/project/storage body: { projectDir, dbBacked }
|
|
331
|
+
// Moves a project between file-backed and DB-backed storage. Toggling on
|
|
332
|
+
// copies the current project object into the DB and leaves any existing
|
|
333
|
+
// .mouaif.json untouched (nothing is deleted). Toggling off writes the DB
|
|
334
|
+
// copy back to .mouaif.json and clears the DB row.
|
|
335
|
+
if (urlPath === '/api/settings/project/storage' && method === 'PUT') {
|
|
336
|
+
const body = await readJsonOr400(req, res);
|
|
337
|
+
if (!body) return;
|
|
338
|
+
const projectDir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
339
|
+
if (!projectDir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
340
|
+
if (typeof body.dbBacked !== 'boolean') {
|
|
341
|
+
return sendJSON(res, 400, { error: 'dbBacked must be a boolean' });
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
if (body.dbBacked) {
|
|
345
|
+
// Seed from the existing file (if any), then flag as DB-backed.
|
|
346
|
+
settings.setDbProject(projectDir, { ...settings.getProjectRaw(projectDir), __dbBacked: true });
|
|
347
|
+
} else {
|
|
348
|
+
const current = settings.getDbProjectRaw(projectDir);
|
|
349
|
+
delete current.__dbBacked;
|
|
350
|
+
settings.writeProjectJson(settings.getProjectPath(projectDir), current);
|
|
351
|
+
// Delete through settings so the key is canonicalized: a raw
|
|
352
|
+
// `DELETE ... WHERE project_dir = ?` on the request path would miss
|
|
353
|
+
// the row when the client sent a trailing slash or `..` segment,
|
|
354
|
+
// leaving isDbBacked() true and the project reading stale DB settings.
|
|
355
|
+
settings.deleteDbProject(projectDir);
|
|
356
|
+
}
|
|
357
|
+
const dbBacked = settings.isDbBacked(projectDir);
|
|
358
|
+
return sendJSON(res, 200, {
|
|
359
|
+
dbBacked,
|
|
360
|
+
project: projectForClient(settings.getProject(projectDir)),
|
|
361
|
+
path: dbBacked ? null : settings.getProjectPath(projectDir)
|
|
362
|
+
});
|
|
363
|
+
} catch (e) {
|
|
364
|
+
return sendJSON(res, 400, { error: e.message });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---- Hide file content (redaction) --------------------------------------
|
|
369
|
+
// Per-project "hide file content" rules (docs/features/hide-file-content.md).
|
|
370
|
+
// The user marks line ranges of a project file that the agent file tools
|
|
371
|
+
// should not reveal. Stored on the project object under `hideFileContent`.
|
|
372
|
+
// GET /api/settings/hide-file-content?projectDir=<abs> -> { rules, project }
|
|
373
|
+
// PUT /api/settings/hide-file-content body { projectDir, rules }
|
|
374
|
+
// GET reads the normalized rules so the UI always sees a clean shape.
|
|
375
|
+
const hideFileContent = require('./hideFileContent.js');
|
|
376
|
+
if (urlPath === '/api/settings/hide-file-content' && method === 'GET') {
|
|
377
|
+
const dir = qs(q, 'projectDir');
|
|
378
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir query param is required' });
|
|
379
|
+
try {
|
|
380
|
+
return sendJSON(res, 200, { rules: hideFileContent.getRules(dir) });
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return sendJSON(res, 500, { error: e.message, code: e.code || 'INTERNAL' });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (urlPath === '/api/settings/hide-file-content' && method === 'PUT') {
|
|
386
|
+
const body = await readJsonOr400(req, res);
|
|
387
|
+
if (!body) return;
|
|
388
|
+
const dir = body && typeof body.projectDir === 'string' ? body.projectDir : '';
|
|
389
|
+
if (!dir) return sendJSON(res, 400, { error: 'projectDir is required' });
|
|
390
|
+
if (!Array.isArray(body.rules)) {
|
|
391
|
+
return sendJSON(res, 400, { error: 'rules must be an array' });
|
|
392
|
+
}
|
|
393
|
+
// Normalize each entry; drop malformed ones. This keeps the stored value
|
|
394
|
+
// canonical so the file-tool reader and this endpoint never disagree.
|
|
395
|
+
const normalized = body.rules.map(hideFileContent.normalizeEntry).filter(Boolean);
|
|
396
|
+
try {
|
|
397
|
+
settings.setProject(dir, { hideFileContent: normalized });
|
|
398
|
+
return sendJSON(res, 200, { rules: hideFileContent.getRules(dir) });
|
|
399
|
+
} catch (e) {
|
|
400
|
+
return sendJSON(res, 400, { error: e.message });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'settings' });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = { handleSettings };
|