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
package/src/prompts.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Custom prompts — user-authored system prompts, stored in app SQLite or per project.
|
|
3
|
+
//
|
|
4
|
+
// Implements the "Custom prompts" feature from .github/copilot-instructions.md §4.
|
|
5
|
+
// Prompts can be stored globally in the app SQLite database (`prompts` key in app settings)
|
|
6
|
+
// or per project in `<projectDir>/.mouaif.json` under a `prompts` array.
|
|
7
|
+
// Project prompts take precedence over app prompts with the same id.
|
|
8
|
+
//
|
|
9
|
+
// Schema (inside app or project.prompts):
|
|
10
|
+
// {
|
|
11
|
+
// id: 'short-kebab-id', // unique within the scope
|
|
12
|
+
// title: 'Concise label', // shown in the UI picker
|
|
13
|
+
// icon: 'sparkles', // safe built-in icon key
|
|
14
|
+
// showOnProjectCard: false, // quick new-chat icon button
|
|
15
|
+
// content: 'You are a helpful…', // the prompt text
|
|
16
|
+
// role: 'system', // locked; the field is kept
|
|
17
|
+
// // for forward-compat and for
|
|
18
|
+
// // hand-edits, but the editor
|
|
19
|
+
// // and validator only accept
|
|
20
|
+
// // 'system'. A non-system role
|
|
21
|
+
// // would have to be prepended
|
|
22
|
+
// // before the transcript, which
|
|
23
|
+
// // is not the intended use.
|
|
24
|
+
// preset: { tools?: ['shell','file',...],
|
|
25
|
+
// agentFiles?: boolean,
|
|
26
|
+
// skills?: boolean }, // OPTIONAL chat preset: when a
|
|
27
|
+
// // chat references this prompt,
|
|
28
|
+
// // these tool + agent-file
|
|
29
|
+
// // settings apply to that chat.
|
|
30
|
+
// // See server-handlers-chats.js
|
|
31
|
+
// // resolveChatPreset. The preset
|
|
32
|
+
// // only *enables* a tool via the
|
|
33
|
+
// // per-chat allowlist and turns
|
|
34
|
+
// // agent files / skills on — it
|
|
35
|
+
// // never overrides the project's
|
|
36
|
+
// // authorization gate (off stays
|
|
37
|
+
// // off).
|
|
38
|
+
// createdAt: '2026-07-15T12:00Z',
|
|
39
|
+
// updatedAt: '2026-07-15T12:00Z'
|
|
40
|
+
// }
|
|
41
|
+
//
|
|
42
|
+
// Each chat can reference a prompt via its `promptId` field (see chats.js).
|
|
43
|
+
// At stream time, the server prepends the prompt as a `system` message
|
|
44
|
+
// before the user message. If the prompt carries a `preset`, the chat's
|
|
45
|
+
// tool / agent-file settings also pick it up (see below).
|
|
46
|
+
const crypto = require('crypto');
|
|
47
|
+
const settings = require('./settings.js');
|
|
48
|
+
|
|
49
|
+
// ---- Helpers ------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function newPromptId() {
|
|
52
|
+
// Kebab-case short id: 4 random hex bytes.
|
|
53
|
+
return crypto.randomBytes(4).toString('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only `system` is accepted. Historical projects may have prompts
|
|
57
|
+
// authored under a different role; normalizeChat silently coerces
|
|
58
|
+
// anything else to 'system' so the field stays valid.
|
|
59
|
+
const VALID_ROLES = new Set(['system']);
|
|
60
|
+
const DEFAULT_ICON = 'sparkles';
|
|
61
|
+
const PROMPT_ICONS = new Set(['sparkles', 'code', 'search', 'pencil', 'bug', 'book']);
|
|
62
|
+
function normalizeIcon(icon) {
|
|
63
|
+
return typeof icon === 'string' && PROMPT_ICONS.has(icon) ? icon : DEFAULT_ICON;
|
|
64
|
+
}
|
|
65
|
+
// The native tool *family* names a prompt preset may list. In addition
|
|
66
|
+
// to these, MCP model-facing tool ids (e.g. `mcp__<slug>__<tool>`) are
|
|
67
|
+
// accepted as-is — the same strings the chat's per-chat tool allowlist
|
|
68
|
+
// stores and that the tool catalog advertises. The preset union-merges
|
|
69
|
+
// them onto `chat.tools`, matching the chat's allowlist semantics.
|
|
70
|
+
//
|
|
71
|
+
// The preset only manipulates the per-chat allowlist — the project's
|
|
72
|
+
// authorization gate (off/ask/allow for `file`, `shell`, …) stays
|
|
73
|
+
// authoritative, so listing a tool here can never turn on something
|
|
74
|
+
// the project turned off.
|
|
75
|
+
const PRESET_TOOL_NAMES = new Set(['shell', 'file', 'subagent', 'report_progress', 'task', 'ask_user', 'webpreview', 'restart_app']);
|
|
76
|
+
|
|
77
|
+
// MCP tool ids in the catalog look like `mcp__<slug>__<tool>`. Anything
|
|
78
|
+
// starting with this prefix is accepted as a tool name in the preset,
|
|
79
|
+
// mirroring `chat.tools`.
|
|
80
|
+
const MCP_TOOL_PREFIX = 'mcp__';
|
|
81
|
+
|
|
82
|
+
// normalizePreset(raw) -> { tools, agentFiles, skills } | null
|
|
83
|
+
//
|
|
84
|
+
// A preset is an OPTIONAL attachment to a prompt. It is normalized to a
|
|
85
|
+
// small, stable shape:
|
|
86
|
+
// - `tools` is a de-duped array of model-facing tool names — the same
|
|
87
|
+
// names the chat's per-chat tool allowlist (`chat.tools`) stores.
|
|
88
|
+
// Native family names (shell, file, subagent, report_progress, task,
|
|
89
|
+
// ask_user) and MCP tool ids (`mcp__<slug>__<tool>`) are both
|
|
90
|
+
// accepted. Anything else is dropped. An empty array means "this
|
|
91
|
+
// preset grants no additional tools" — equivalent to omitting the
|
|
92
|
+
// field, but kept when the user explicitly cleared it.
|
|
93
|
+
// - `agentFiles` is a boolean: when true the chat referencing this
|
|
94
|
+
// prompt turns agent-file injection on.
|
|
95
|
+
// - `skills` is a boolean: when true the chat referencing this prompt
|
|
96
|
+
// turns skill injection on. Matches the per-chat `skills` toggle and
|
|
97
|
+
// the project-level `skills` lock.
|
|
98
|
+
// Anything not an object, or with none of the three fields, collapses to
|
|
99
|
+
// null (treated as "no preset").
|
|
100
|
+
function normalizePreset(raw) {
|
|
101
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
102
|
+
const tools = Array.isArray(raw.tools)
|
|
103
|
+
? Array.from(new Set(raw.tools
|
|
104
|
+
.filter((t) => typeof t === 'string' && (PRESET_TOOL_NAMES.has(t) || t.startsWith(MCP_TOOL_PREFIX)))
|
|
105
|
+
.map((t) => t)))
|
|
106
|
+
: undefined;
|
|
107
|
+
const agentFiles = typeof raw.agentFiles === 'boolean' ? raw.agentFiles : undefined;
|
|
108
|
+
const skills = typeof raw.skills === 'boolean' ? raw.skills : undefined;
|
|
109
|
+
|
|
110
|
+
if ((!tools || !tools.length) && agentFiles === undefined && skills === undefined) return null;
|
|
111
|
+
return {
|
|
112
|
+
tools: tools || undefined,
|
|
113
|
+
agentFiles,
|
|
114
|
+
skills
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizePrompt(raw) {
|
|
119
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
120
|
+
if (typeof raw.id !== 'string' || !raw.id) return null;
|
|
121
|
+
if (typeof raw.content !== 'string' || !raw.content.trim()) return null;
|
|
122
|
+
return {
|
|
123
|
+
id: raw.id,
|
|
124
|
+
title: typeof raw.title === 'string' && raw.title.trim() ? raw.title.trim() : raw.id,
|
|
125
|
+
icon: normalizeIcon(raw.icon),
|
|
126
|
+
showOnProjectCard: raw.showOnProjectCard === true,
|
|
127
|
+
content: raw.content,
|
|
128
|
+
role: 'system',
|
|
129
|
+
preset: normalizePreset(raw.preset),
|
|
130
|
+
createdAt: raw.createdAt || new Date().toISOString(),
|
|
131
|
+
updatedAt: raw.updatedAt || raw.createdAt || new Date().toISOString()
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- CRUD ---------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function getPromptsList(projectDir) {
|
|
138
|
+
if (projectDir) {
|
|
139
|
+
const project = settings.getProject(projectDir);
|
|
140
|
+
return Array.isArray(project.prompts) ? project.prompts : [];
|
|
141
|
+
}
|
|
142
|
+
const app = settings.getApp();
|
|
143
|
+
return Array.isArray(app.prompts) ? app.prompts : [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function savePromptsList(projectDir, list) {
|
|
147
|
+
if (projectDir) {
|
|
148
|
+
settings.setProject(projectDir, { prompts: list });
|
|
149
|
+
} else {
|
|
150
|
+
settings.setApp({ prompts: list });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function listPrompts(projectDir, opts = {}) {
|
|
155
|
+
// If no projectDir is provided, returns app-level prompts with scope: 'app'.
|
|
156
|
+
if (!projectDir) {
|
|
157
|
+
const raw = getPromptsList('');
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const p of raw) {
|
|
160
|
+
const n = normalizePrompt(p);
|
|
161
|
+
if (n) out.push({ ...n, scope: 'app' });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If explicit scope is requested:
|
|
167
|
+
if (opts && opts.scope === 'project') {
|
|
168
|
+
const raw = getPromptsList(projectDir);
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const p of raw) {
|
|
171
|
+
const n = normalizePrompt(p);
|
|
172
|
+
if (n) out.push({ ...n, scope: 'project' });
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
if (opts && opts.scope === 'app') {
|
|
177
|
+
const raw = getPromptsList('');
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const p of raw) {
|
|
180
|
+
const n = normalizePrompt(p);
|
|
181
|
+
if (n) out.push({ ...n, scope: 'app' });
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Merged: project prompts take precedence over app prompts with the same id.
|
|
187
|
+
const appRaw = getPromptsList('');
|
|
188
|
+
const projRaw = getPromptsList(projectDir);
|
|
189
|
+
const out = [];
|
|
190
|
+
const seenIds = new Set();
|
|
191
|
+
|
|
192
|
+
for (const p of projRaw) {
|
|
193
|
+
const n = normalizePrompt(p);
|
|
194
|
+
if (n) {
|
|
195
|
+
out.push({ ...n, scope: 'project' });
|
|
196
|
+
seenIds.add(n.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const p of appRaw) {
|
|
200
|
+
const n = normalizePrompt(p);
|
|
201
|
+
if (n && !seenIds.has(n.id)) {
|
|
202
|
+
out.push({ ...n, scope: 'app' });
|
|
203
|
+
seenIds.add(n.id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getPrompt(projectDir, promptId, opts = {}) {
|
|
210
|
+
if (!promptId || typeof promptId !== 'string') return null;
|
|
211
|
+
return listPrompts(projectDir, opts).find(p => p.id === promptId) || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createPrompt(projectDir, opts) {
|
|
215
|
+
if (!opts || typeof opts.content !== 'string' || !opts.content.trim()) {
|
|
216
|
+
const e = new Error('content is required');
|
|
217
|
+
e.code = 'EBADINPUT';
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
const targetDir = (opts && opts.scope === 'app') ? '' : (projectDir || '');
|
|
221
|
+
const rawList = getPromptsList(targetDir);
|
|
222
|
+
const list = rawList.slice();
|
|
223
|
+
const prompt = normalizePrompt({
|
|
224
|
+
id: typeof opts.id === 'string' && opts.id.trim() ? opts.id.trim() : newPromptId(),
|
|
225
|
+
title: typeof opts.title === 'string' && opts.title.trim() ? opts.title.trim() : '',
|
|
226
|
+
icon: normalizeIcon(opts.icon),
|
|
227
|
+
showOnProjectCard: opts.showOnProjectCard === true,
|
|
228
|
+
content: opts.content,
|
|
229
|
+
role: 'system',
|
|
230
|
+
preset: normalizePreset(opts.preset),
|
|
231
|
+
createdAt: new Date().toISOString(),
|
|
232
|
+
updatedAt: new Date().toISOString()
|
|
233
|
+
});
|
|
234
|
+
if (!prompt) {
|
|
235
|
+
const e = new Error('Failed to normalize prompt');
|
|
236
|
+
e.code = 'EBADINPUT';
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
list.push(prompt);
|
|
240
|
+
savePromptsList(targetDir, list);
|
|
241
|
+
return { ...prompt, scope: targetDir ? 'project' : 'app' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function updatePrompt(projectDir, promptId, patch) {
|
|
245
|
+
if (!promptId) return null;
|
|
246
|
+
const targetDir = (patch && patch.scope === 'app') ? '' : (projectDir || '');
|
|
247
|
+
const rawList = getPromptsList(targetDir);
|
|
248
|
+
const list = rawList.slice();
|
|
249
|
+
let idx = list.findIndex(p => p && p.id === promptId);
|
|
250
|
+
let effectiveDir = targetDir;
|
|
251
|
+
|
|
252
|
+
if (idx < 0 && targetDir && (!patch || !patch.scope)) {
|
|
253
|
+
const appList = getPromptsList('').slice();
|
|
254
|
+
const appIdx = appList.findIndex(p => p && p.id === promptId);
|
|
255
|
+
if (appIdx >= 0) {
|
|
256
|
+
idx = appIdx;
|
|
257
|
+
effectiveDir = '';
|
|
258
|
+
list.length = 0;
|
|
259
|
+
list.push(...appList);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (idx < 0) return null;
|
|
263
|
+
|
|
264
|
+
const current = normalizePrompt(list[idx]);
|
|
265
|
+
if (!current) return null;
|
|
266
|
+
if (patch && typeof patch.title === 'string') {
|
|
267
|
+
current.title = patch.title.trim() || current.id;
|
|
268
|
+
}
|
|
269
|
+
if (patch && typeof patch.content === 'string' && patch.content.trim()) {
|
|
270
|
+
current.content = patch.content;
|
|
271
|
+
}
|
|
272
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'icon')) {
|
|
273
|
+
current.icon = normalizeIcon(patch.icon);
|
|
274
|
+
}
|
|
275
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'showOnProjectCard')) {
|
|
276
|
+
current.showOnProjectCard = patch.showOnProjectCard === true;
|
|
277
|
+
}
|
|
278
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'preset')) {
|
|
279
|
+
current.preset = patch.preset == null ? null : normalizePreset(patch.preset);
|
|
280
|
+
}
|
|
281
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'role') && !VALID_ROLES.has(patch.role)) {
|
|
282
|
+
// no-op
|
|
283
|
+
}
|
|
284
|
+
current.updatedAt = new Date().toISOString();
|
|
285
|
+
list[idx] = current;
|
|
286
|
+
savePromptsList(effectiveDir, list);
|
|
287
|
+
return { ...current, scope: effectiveDir ? 'project' : 'app' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function deletePrompt(projectDir, promptId, opts) {
|
|
291
|
+
if (!promptId) return false;
|
|
292
|
+
const targetDir = (opts && opts.scope === 'app') ? '' : (projectDir || '');
|
|
293
|
+
const rawList = getPromptsList(targetDir);
|
|
294
|
+
let list = rawList.slice();
|
|
295
|
+
let before = list.length;
|
|
296
|
+
list = list.filter(p => p && p.id !== promptId);
|
|
297
|
+
let effectiveDir = targetDir;
|
|
298
|
+
|
|
299
|
+
if (list.length === before) {
|
|
300
|
+
if (targetDir && (!opts || !opts.scope)) {
|
|
301
|
+
const appRaw = getPromptsList('');
|
|
302
|
+
const appList = appRaw.filter(p => p && p.id !== promptId);
|
|
303
|
+
if (appList.length !== appRaw.length) {
|
|
304
|
+
savePromptsList('', appList);
|
|
305
|
+
if (opts && typeof opts.onRemoved === 'function') {
|
|
306
|
+
try { opts.onRemoved(promptId); } catch (e) { /* swallow */ }
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
savePromptsList(effectiveDir, list);
|
|
315
|
+
if (opts && typeof opts.onRemoved === 'function') {
|
|
316
|
+
try { opts.onRemoved(promptId); } catch (e) { /* swallow */ }
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// getPromptPreset(projectDir, promptId) -> { tools, agentFiles } | null
|
|
322
|
+
//
|
|
323
|
+
// Convenience for the chat pipeline: resolves a prompt's preset without
|
|
324
|
+
// callers having to reach into getPrompt()'s shape. Returns null when the
|
|
325
|
+
// prompt is missing or carries no preset. Missing/corrupt project files
|
|
326
|
+
// resolve to null so a bad preset never breaks the tool loop.
|
|
327
|
+
function getPromptPreset(projectDir, promptId) {
|
|
328
|
+
if (!promptId) return null;
|
|
329
|
+
try {
|
|
330
|
+
const p = getPrompt(projectDir, promptId);
|
|
331
|
+
return p && p.preset ? p.preset : null;
|
|
332
|
+
} catch { return null; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// effectivePresetConfig(chat, preset) -> { tools?, agentFiles?, skills? }
|
|
336
|
+
//
|
|
337
|
+
// Merge a prompt's preset onto a chat's own per-chat config. Returns a
|
|
338
|
+
// partial update object the chat pipeline reads as the effective values.
|
|
339
|
+
//
|
|
340
|
+
// Tools are ADDITIVE, never downgrading: a chat with no per-chat
|
|
341
|
+
// allowlist already inherits every project tool, so emitting the preset's
|
|
342
|
+
// list here would silently restrict the chat to just those tools — the
|
|
343
|
+
// opposite of "enable". Only when the chat already restricts its tools
|
|
344
|
+
// does the preset union its list in. Absent preset fields are ignored, so
|
|
345
|
+
// a preset granting only tools leaves the chat's agentFiles untouched and
|
|
346
|
+
// vice-versa.
|
|
347
|
+
//
|
|
348
|
+
// The preset is a convenience layer over the existing per-chat settings —
|
|
349
|
+
// it never relaxes the project's authorization gate (a tool the project
|
|
350
|
+
// turned `off` stays off; agent files and skills silently follow the
|
|
351
|
+
// project lock).
|
|
352
|
+
function effectivePresetConfig(chat, preset) {
|
|
353
|
+
const out = {};
|
|
354
|
+
if (chat && typeof chat === 'object' && preset && typeof preset === 'object') {
|
|
355
|
+
if (Array.isArray(preset.tools)) {
|
|
356
|
+
// Merge ONLY onto an existing per-chat allowlist. `chat.tools`
|
|
357
|
+
// being undefined means "all project tools" — leave it that way.
|
|
358
|
+
if (Array.isArray(chat.tools)) {
|
|
359
|
+
out.tools = Array.from(new Set(chat.tools.concat(preset.tools)));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (typeof preset.agentFiles === 'boolean') out.agentFiles = preset.agentFiles;
|
|
363
|
+
if (typeof preset.skills === 'boolean') out.skills = preset.skills;
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
newPromptId,
|
|
370
|
+
VALID_ROLES,
|
|
371
|
+
DEFAULT_ICON,
|
|
372
|
+
PROMPT_ICONS,
|
|
373
|
+
normalizeIcon,
|
|
374
|
+
PRESET_TOOL_NAMES,
|
|
375
|
+
MCP_TOOL_PREFIX,
|
|
376
|
+
normalizePreset,
|
|
377
|
+
listPrompts,
|
|
378
|
+
getPrompt,
|
|
379
|
+
getPromptPreset,
|
|
380
|
+
effectivePresetConfig,
|
|
381
|
+
createPrompt,
|
|
382
|
+
updatePrompt,
|
|
383
|
+
deletePrompt
|
|
384
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// providerShapes — which wire protocol a provider *connection* speaks.
|
|
4
|
+
//
|
|
5
|
+
// This is a leaf module: it requires nothing, so the domain modules that
|
|
6
|
+
// need the answer (src/transcribe.js for dictation, and any future
|
|
7
|
+
// non-chat model product) can pull from it without dragging in the chat
|
|
8
|
+
// provider registry (src/ai-endpoints.js) or creating a require cycle.
|
|
9
|
+
//
|
|
10
|
+
// The list used to be copy-pasted into each of those two modules, which
|
|
11
|
+
// made adding a provider a three-place edit and let the two copies drift:
|
|
12
|
+
// transcribe's copy had grown an extra `anthropic` entry that the wire
|
|
13
|
+
// claim does not hold for — Anthropic serves its own Messages API shape,
|
|
14
|
+
// not the OpenAI-shaped multipart or `/chat/completions` surface.
|
|
15
|
+
//
|
|
16
|
+
// The *shipped* OpenAI-shaped providers are exactly the entries in
|
|
17
|
+
// src/ai-endpoints.js `ENDPOINTS` whose base URL speaks `/chat/completions`
|
|
18
|
+
// and `/models`: the openai-compatible family, OpenRouter, Azure, Mistral,
|
|
19
|
+
// Groq, DeepSeek, Ollama, and GitHub Copilot. Gemini is absent because it
|
|
20
|
+
// speaks its own per-model `generateContent` action path, and Anthropic is
|
|
21
|
+
// absent because it speaks the Messages API — both are addressed by their
|
|
22
|
+
// own request builders, never by an OpenAI-shaped one.
|
|
23
|
+
//
|
|
24
|
+
// Adding a provider: an `ENDPOINTS` row and a builder in src/ai-endpoints.js
|
|
25
|
+
// is the substantive change; add its id here too if its base URL really is
|
|
26
|
+
// OpenAI-shaped.
|
|
27
|
+
const OPENAI_SHAPED_PROVIDERS = Object.freeze([
|
|
28
|
+
'openai-compatible', 'openrouter', 'azure', 'mistral', 'groq', 'deepseek',
|
|
29
|
+
'ollama', 'github-copilot'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// isOpenAIShaped(providerId) — does this connection speak the OpenAI shape?
|
|
33
|
+
// Absent or unknown providers are "no": a shape we cannot vouch for is not
|
|
34
|
+
// a shape, and every caller that asks this question has a conservative
|
|
35
|
+
// default (the conventional endpoint, or "not a candidate") to fall back on.
|
|
36
|
+
function isOpenAIShaped(provider) {
|
|
37
|
+
const id = typeof provider === 'string' ? provider : '';
|
|
38
|
+
return id !== '' && OPENAI_SHAPED_PROVIDERS.includes(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
OPENAI_SHAPED_PROVIDERS,
|
|
43
|
+
isOpenAIShaped
|
|
44
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Base adapter interface and common utilities for AI provider implementations.
|
|
3
|
+
|
|
4
|
+
class BaseAIProvider {
|
|
5
|
+
constructor(id, options = {}) {
|
|
6
|
+
this.id = id;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Normalize usage / token counts from upstream
|
|
11
|
+
normalizeUsage(rawUsage) {
|
|
12
|
+
if (!rawUsage || typeof rawUsage !== 'object') {
|
|
13
|
+
return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
inputTokens: rawUsage.prompt_tokens || rawUsage.input_tokens || 0,
|
|
17
|
+
outputTokens: rawUsage.completion_tokens || rawUsage.output_tokens || 0,
|
|
18
|
+
cacheReadTokens: rawUsage.cache_read_input_tokens || (rawUsage.prompt_tokens_details && rawUsage.prompt_tokens_details.cached_tokens) || rawUsage.prompt_cache_hit_tokens || rawUsage.cached_tokens || 0,
|
|
19
|
+
cacheWriteTokens: rawUsage.cache_creation_input_tokens || 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Format tools to provider-specific schema
|
|
24
|
+
formatTools(tools) {
|
|
25
|
+
return tools;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Hook for request building
|
|
29
|
+
buildRequest(payload) {
|
|
30
|
+
throw new Error(`buildRequest not implemented for provider ${this.id}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Hook for event parsing
|
|
34
|
+
parseEvent(event, data, state) {
|
|
35
|
+
throw new Error(`parseEvent not implemented for provider ${this.id}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
BaseAIProvider
|
|
41
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Registry for AI providers in mouaif.
|
|
3
|
+
|
|
4
|
+
const { BaseAIProvider } = require('./base.js');
|
|
5
|
+
|
|
6
|
+
const providerRegistry = new Map();
|
|
7
|
+
|
|
8
|
+
function registerProvider(id, providerInstance) {
|
|
9
|
+
providerRegistry.set(id, providerInstance);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getProvider(id) {
|
|
13
|
+
return providerRegistry.get(id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listRegisteredProviders() {
|
|
17
|
+
return Array.from(providerRegistry.keys());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
BaseAIProvider,
|
|
22
|
+
registerProvider,
|
|
23
|
+
getProvider,
|
|
24
|
+
listRegisteredProviders
|
|
25
|
+
};
|