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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/bin/mouaif.js +281 -0
  4. package/frontend/dist/assets/AgentFilePicker-CcKLJorU.js +1 -0
  5. package/frontend/dist/assets/CliModal-Hs5phmNZ.js +7 -0
  6. package/frontend/dist/assets/DictationPage-BI23lp42.js +2 -0
  7. package/frontend/dist/assets/FileEditor-DDl31c6d.js +2 -0
  8. package/frontend/dist/assets/GitModal-3EC_gpJ5.js +2 -0
  9. package/frontend/dist/assets/Inspector-Ba3R1w04.js +73 -0
  10. package/frontend/dist/assets/SettingsAbout-bvZGDEDw.js +1 -0
  11. package/frontend/dist/assets/SettingsActions-Dk6WX9jv.js +1 -0
  12. package/frontend/dist/assets/SettingsAgents-BNV0MgDB.js +1 -0
  13. package/frontend/dist/assets/SettingsDefaults-DbMmQbzc.js +1 -0
  14. package/frontend/dist/assets/SettingsHiddenContent-BZ2sloH1.js +1 -0
  15. package/frontend/dist/assets/SettingsMcp-DOrfbQd1.js +1 -0
  16. package/frontend/dist/assets/SettingsMcpEdit-BGMQ2CWC.js +3 -0
  17. package/frontend/dist/assets/SettingsMcpRegistry-BywXee_A.js +1 -0
  18. package/frontend/dist/assets/SettingsNotifications-B0LEs11a.js +1 -0
  19. package/frontend/dist/assets/SettingsPricing-BAg33iVF.js +1 -0
  20. package/frontend/dist/assets/SettingsProject-DNrKhCcZ.js +14 -0
  21. package/frontend/dist/assets/SettingsProjects-IqkBfDcm.js +1 -0
  22. package/frontend/dist/assets/SettingsPrompts-BgeiASuk.js +1 -0
  23. package/frontend/dist/assets/SettingsProviders-k0xJN0IK.js +1 -0
  24. package/frontend/dist/assets/SettingsTags-B5kjFdQi.js +1 -0
  25. package/frontend/dist/assets/agentNavigation-BiiCpFz5.js +1 -0
  26. package/frontend/dist/assets/codemirror-Bp6CUUFk.js +30 -0
  27. package/frontend/dist/assets/index-BGvI4n0T.js +61 -0
  28. package/frontend/dist/assets/index-Bgg1gnDf.css +1 -0
  29. package/frontend/dist/assets/index-C1sQFIC-.css +1 -0
  30. package/frontend/dist/assets/index-CANPYzQg.css +1 -0
  31. package/frontend/dist/assets/index-Crn1LdzK.css +1 -0
  32. package/frontend/dist/assets/index-FbCWDPiB.css +1 -0
  33. package/frontend/dist/assets/projectQS-D1cSZ7Gr.js +1 -0
  34. package/frontend/dist/assets/virtual-list-6H9b4K51.js +1 -0
  35. package/frontend/dist/icons/favicon-32.png +0 -0
  36. package/frontend/dist/icons/icon-180-apple.png +0 -0
  37. package/frontend/dist/icons/icon-192.png +0 -0
  38. package/frontend/dist/icons/icon-512.png +0 -0
  39. package/frontend/dist/icons/icon-maskable-512.png +0 -0
  40. package/frontend/dist/index.html +83 -0
  41. package/frontend/dist/manifest.webmanifest +33 -0
  42. package/frontend/dist/sw.js +482 -0
  43. package/package.json +98 -0
  44. package/scripts/patch-zimmerframe.js +58 -0
  45. package/src/access-auth.js +515 -0
  46. package/src/agentFeatures.js +294 -0
  47. package/src/agentFiles.js +164 -0
  48. package/src/agentSkills.js +147 -0
  49. package/src/agents.js +230 -0
  50. package/src/ai-chat.js +21 -0
  51. package/src/ai-endpoints.js +1880 -0
  52. package/src/ai-stream.js +2048 -0
  53. package/src/ai.js +68 -0
  54. package/src/auth.js +391 -0
  55. package/src/chatdb.js +816 -0
  56. package/src/chats.js +275 -0
  57. package/src/custom-actions.js +65 -0
  58. package/src/files.js +431 -0
  59. package/src/hideFileContent.js +327 -0
  60. package/src/http-server.js +535 -0
  61. package/src/index.js +15 -0
  62. package/src/inspector.js +731 -0
  63. package/src/inspectorProfiles.js +503 -0
  64. package/src/live-chat.js +107 -0
  65. package/src/mcp.js +1517 -0
  66. package/src/messages.js +238 -0
  67. package/src/modelList.js +137 -0
  68. package/src/notifications.js +52 -0
  69. package/src/oauth-anthropic.js +280 -0
  70. package/src/oauth-github-copilot.js +417 -0
  71. package/src/oauth-mcp.js +216 -0
  72. package/src/oauth-openrouter.js +285 -0
  73. package/src/package-version.js +20 -0
  74. package/src/projects.js +285 -0
  75. package/src/promptProfiles.js +256 -0
  76. package/src/prompts.js +384 -0
  77. package/src/providerShapes.js +44 -0
  78. package/src/providers/base.js +41 -0
  79. package/src/providers/index.js +25 -0
  80. package/src/push.js +315 -0
  81. package/src/qr.js +192 -0
  82. package/src/restart.js +47 -0
  83. package/src/server-handlers-access.js +306 -0
  84. package/src/server-handlers-actions.js +100 -0
  85. package/src/server-handlers-ai.js +248 -0
  86. package/src/server-handlers-auth.js +273 -0
  87. package/src/server-handlers-chats.js +1436 -0
  88. package/src/server-handlers-git.js +467 -0
  89. package/src/server-handlers-mcp-oauth.js +56 -0
  90. package/src/server-handlers-misc.js +783 -0
  91. package/src/server-handlers-projects.js +289 -0
  92. package/src/server-handlers-prompts.js +259 -0
  93. package/src/server-handlers-push.js +102 -0
  94. package/src/server-handlers-settings.js +406 -0
  95. package/src/server-handlers-tools.js +654 -0
  96. package/src/server-handlers-transcribe.js +399 -0
  97. package/src/server-shared.js +780 -0
  98. package/src/server-web-static.js +191 -0
  99. package/src/settings.js +898 -0
  100. package/src/statusBar.js +541 -0
  101. package/src/tags.js +414 -0
  102. package/src/toolFeedback.js +225 -0
  103. package/src/tools/ask.js +154 -0
  104. package/src/tools/authorization.js +932 -0
  105. package/src/tools/files.js +1150 -0
  106. package/src/tools/progress.js +71 -0
  107. package/src/tools/restart.js +32 -0
  108. package/src/tools/searchEngine.js +957 -0
  109. package/src/tools/shell.js +341 -0
  110. package/src/tools/subagent.js +47 -0
  111. package/src/tools/task.js +234 -0
  112. package/src/tools/webpreview.js +448 -0
  113. package/src/trace.js +103 -0
  114. package/src/transcribe.js +683 -0
  115. package/src/usage.js +389 -0
  116. 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
+ };