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
@@ -0,0 +1,273 @@
1
+ 'use strict';
2
+
3
+ // Auth + OAuth loopback callback REST handlers. Extracted from the
4
+ // original single-file http-server.js. Shared helpers live in
5
+ // src/server-shared.js.
6
+
7
+ const {
8
+ sendJSON,
9
+ qs,
10
+ readJsonOr400,
11
+ htmlPage,
12
+ redirectMeta,
13
+ finishOAuth,
14
+ expectedOrigin,
15
+ DEFAULT_PORT,
16
+ auth,
17
+ oauthAnthropic,
18
+ oauthCopilot,
19
+ oauthOpenRouter,
20
+ safeDecode
21
+ } = require('./server-shared.js');
22
+
23
+ // The URL the OAuth provider must redirect the user's browser back to.
24
+ // The old hard-coded loopback (`http://127.0.0.1:<port>/oauth/callback`)
25
+ // only works when the app is served from the same machine the user is on.
26
+ // Behind a domain name (reverse proxy, --public-origin / MOUAIF_PUBLIC_ORIGIN)
27
+ // the provider would bounce the browser to the *user's* localhost, which is
28
+ // not the server. Derive the origin from the request instead: the public
29
+ // origin wins when configured, otherwise the Host header (which a proxy
30
+ // forwards by default). Falls back to 127.0.0.1 only when neither exists.
31
+ function defaultCallbackUrl(req, publicOrigin) {
32
+ const origin = expectedOrigin(req, publicOrigin);
33
+ if (origin) return origin + '/oauth/callback';
34
+ const port = req.socket.address() && req.socket.address().port;
35
+ return 'http://127.0.0.1:' + (port || DEFAULT_PORT) + '/oauth/callback';
36
+ }
37
+
38
+ async function handleAuth(req, res, parsed, serverConfig) {
39
+ const urlPath = parsed.pathname;
40
+ const method = req.method;
41
+ const q = parsed.query || {};
42
+ const publicOrigin = serverConfig && serverConfig.publicOrigin;
43
+
44
+ if (urlPath === '/api/auth/accounts' && method === 'GET') {
45
+ return sendJSON(res, 200, { accounts: auth.listAccounts() });
46
+ }
47
+
48
+ if (urlPath === '/api/auth/status' && method === 'GET') {
49
+ const provider = qs(q, 'provider');
50
+ if (!provider) return sendJSON(res, 400, { error: 'provider query param is required' });
51
+ if (!auth.SUPPORTED_PROVIDERS.includes(provider)) {
52
+ return sendJSON(res, 400, { error: 'Unknown provider', provider });
53
+ }
54
+ const accounts = auth.listAccounts();
55
+ return sendJSON(res, 200, {
56
+ provider,
57
+ accounts: accounts[provider] || [],
58
+ hasExchange: !!auth.getExchange(provider)
59
+ });
60
+ }
61
+
62
+ const delMatch = urlPath.match(/^\/api\/auth\/accounts\/([a-z0-9-]+)\/(.+)$/);
63
+ if (delMatch && method === 'DELETE') {
64
+ const provider = delMatch[1];
65
+ const account = safeDecode(delMatch[2]);
66
+ if (!auth.SUPPORTED_PROVIDERS.includes(provider)) {
67
+ return sendJSON(res, 400, { error: 'Unknown provider', provider });
68
+ }
69
+ try {
70
+ const r = auth.deleteToken(provider, account);
71
+ return sendJSON(res, 200, Object.assign({ ok: true }, r));
72
+ } catch (e) {
73
+ return sendJSON(res, 500, { error: e.message, code: e.code || 'EKEYRING' });
74
+ }
75
+ }
76
+
77
+ // POST /api/auth/sign-in/anthropic -> { authorizeUrl, state, expiresAt }
78
+ // The UI calls this, opens authorizeUrl in the user's browser, and
79
+ // polls GET /api/auth/status?provider=anthropic until hasExchange
80
+ // (already registered) and accounts include the signed-in email.
81
+ if (urlPath === '/api/auth/sign-in/anthropic' && method === 'POST') {
82
+ if (!auth.getExchange('anthropic')) {
83
+ return sendJSON(res, 501, { error: 'Anthropic OAuth is not registered in this build' });
84
+ }
85
+ const body = await readJsonOr400(req, res);
86
+ if (!body) return;
87
+
88
+ const state = oauthAnthropic.newState();
89
+ const verifier = oauthAnthropic.newVerifier();
90
+ const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
91
+ // The callback handler is shared by providers and therefore requires the
92
+ // provider name. OAuth providers return our redirect URI verbatim, so
93
+ // bind the provider into it before recording the pending exchange.
94
+ if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'anthropic');
95
+ const redirectUri = callbackUrl.toString();
96
+ const scope = body.scope || oauthAnthropic.DEFAULT_SCOPE;
97
+
98
+ auth.recordPending('anthropic', {
99
+ state,
100
+ codeVerifier: verifier,
101
+ redirectUri,
102
+ scopes: scope,
103
+ accountHint: body.accountHint || ''
104
+ });
105
+
106
+ const authorizeUrl = oauthAnthropic.buildAuthorizeUrl({
107
+ redirectUri,
108
+ state,
109
+ verifier,
110
+ scope
111
+ });
112
+
113
+ return sendJSON(res, 200, {
114
+ authorizeUrl,
115
+ redirectUri,
116
+ state,
117
+ expiresAt: Date.now() + 5 * 60 * 1000,
118
+ // Echoed for debugging; the production base is https://api.anthropic.com
119
+ // unless MOUAIF_ANTHROPIC_API_BASE is set (test override).
120
+ apiBase: oauthAnthropic.DEFAULT_API_BASE
121
+ });
122
+ }
123
+
124
+ // POST /api/auth/sign-in/github-copilot -> { authorizeUrl, state, expiresAt }
125
+ // GitHub Copilot uses the standard GitHub OAuth web flow with PKCE
126
+ // (decision §12: each provider picks its own auth shape). The user
127
+ // signs in at github.com/login/oauth/authorize, gets redirected
128
+ // back to /oauth/callback, the server exchanges the code for a
129
+ // long-lived GitHub OAuth access token, and the per-request
130
+ // Copilot API token is derived on demand by src/ai.js.
131
+ if (urlPath === '/api/auth/sign-in/github-copilot' && method === 'POST') {
132
+ if (!auth.getExchange('github-copilot')) {
133
+ return sendJSON(res, 501, { error: 'GitHub Copilot OAuth is not registered in this build' });
134
+ }
135
+ const body = await readJsonOr400(req, res);
136
+ if (!body) return;
137
+
138
+ const state = oauthCopilot.newState();
139
+ const verifier = oauthCopilot.newVerifier();
140
+ const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
141
+ if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'github-copilot');
142
+ const redirectUri = callbackUrl.toString();
143
+ const scope = body.scope || oauthCopilot.DEFAULT_SCOPE;
144
+
145
+ auth.recordPending('github-copilot', {
146
+ state,
147
+ codeVerifier: verifier,
148
+ redirectUri,
149
+ scopes: scope,
150
+ accountHint: body.accountHint || ''
151
+ });
152
+
153
+ const authorizeUrl = oauthCopilot.buildAuthorizeUrl({
154
+ redirectUri,
155
+ state,
156
+ verifier,
157
+ scope
158
+ });
159
+
160
+ return sendJSON(res, 200, {
161
+ authorizeUrl,
162
+ redirectUri,
163
+ state,
164
+ expiresAt: Date.now() + 10 * 60 * 1000,
165
+ // Echoed for debugging; the production base is the default.
166
+ apiBase: oauthCopilot.COPILOT_API_BASE
167
+ });
168
+ }
169
+
170
+ // POST /api/auth/sign-in/openrouter -> { authorizeUrl, state, expiresAt }
171
+ // OpenRouter's PKCE flow (docs/decisions.md §12). Unlike
172
+ // Anthropic / GitHub Copilot, the loopback URL the user is
173
+ // redirected back to is the same URL we pass in — OpenRouter
174
+ // echoes it via the `callback_url` query param rather than
175
+ // expecting a pre-registered redirect. The user signs in at
176
+ // openrouter.ai/auth and OpenRouter redirects back to our
177
+ // /oauth/callback with `?code=...&state=...`. The server
178
+ // exchanges the code for a user-controlled OpenRouter API key
179
+ // (src/oauth-openrouter.js) and stores it in the keyring under
180
+ // the `openrouter` namespace. Subsequent chats use the same
181
+ // Bearer header path as a manually pasted OpenRouter key.
182
+ if (urlPath === '/api/auth/sign-in/openrouter' && method === 'POST') {
183
+ if (!auth.getExchange('openrouter')) {
184
+ return sendJSON(res, 501, { error: 'OpenRouter OAuth is not registered in this build' });
185
+ }
186
+ const body = await readJsonOr400(req, res);
187
+ if (!body) return;
188
+
189
+ const rawState = oauthOpenRouter.newState();
190
+ // Encode the provider in the state prefix so it survives the
191
+ // OAuth redirect. OpenRouter does not forward ?provider= from
192
+ // the callback_url, so we need the provider in a field the
193
+ // IdP echoes back faithfully. See finishOAuth()'s state parsing.
194
+ const state = 'openrouter:' + rawState;
195
+ const verifier = oauthOpenRouter.newVerifier();
196
+ const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
197
+ if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'openrouter');
198
+ const redirectUri = callbackUrl.toString();
199
+
200
+ auth.recordPending('openrouter', {
201
+ state,
202
+ codeVerifier: verifier,
203
+ redirectUri,
204
+ scopes: 'openrouter',
205
+ accountHint: body.accountHint || ''
206
+ });
207
+
208
+ const authorizeUrl = oauthOpenRouter.buildAuthorizeUrl({
209
+ callbackUrl: redirectUri,
210
+ state,
211
+ verifier
212
+ });
213
+
214
+ return sendJSON(res, 200, {
215
+ authorizeUrl,
216
+ redirectUri,
217
+ state,
218
+ expiresAt: Date.now() + 10 * 60 * 1000,
219
+ apiBase: oauthOpenRouter.KEYS_URL
220
+ });
221
+ }
222
+
223
+ return sendJSON(res, 404, { error: 'Not found', scope: 'auth' });
224
+ }
225
+
226
+ async function handleOAuthCallback(req, res, parsed) {
227
+ const q = parsed.query || {};
228
+ const provider = qs(q, 'provider');
229
+ const state = qs(q, 'state');
230
+ const code = qs(q, 'code');
231
+ const errorParam = qs(q, 'error');
232
+
233
+ const result = await finishOAuth({ provider, state, code, errorParam, format: 'html' });
234
+ res.writeHead(result.status, { 'Content-Type': 'text/html; charset=utf-8' });
235
+ // On iOS (especially PWA standalone mode) the popup that opened the OAuth
236
+ // provider may be the current page itself — there is no other tab to close.
237
+ // The redirect is relative (same origin) so the existing session cookie
238
+ // carries over.
239
+ const returnUrl = '/';
240
+ // Success pages return instantly: close the popup (the opener app polls
241
+ // the account list and reports success), or location.replace the full
242
+ // page back into the app. Error pages keep only the plain 2s meta refresh
243
+ // so the user can read what went wrong before the page returns to the app.
244
+ if (result.status === 200) {
245
+ res.end(htmlPage('Signed in', '<p class="ok">Signed in to <code>' + provider + '</code> as <code>' + result.account + '</code>.</p>'
246
+ + redirectMeta(returnUrl, true)));
247
+ } else if (result.code === 'EPROVIDER_ERROR') {
248
+ res.end(htmlPage('Sign-in failed', '<p class="err">' + result.error + '</p>'
249
+ + redirectMeta(returnUrl)));
250
+ } else if (result.code === 'ENOEXCHANGE') {
251
+ res.end(htmlPage('OAuth not configured', '<p class="err">Sign-in for <code>' + provider + '</code> is not configured in this build. A later commit will register the provider exchange.</p>'
252
+ + redirectMeta(returnUrl)));
253
+ } else {
254
+ res.end(htmlPage('OAuth callback', '<p class="err">' + (result.error || 'unknown error') + '</p>'
255
+ + redirectMeta(returnUrl)));
256
+ }
257
+ }
258
+
259
+ async function handleOAuthCallbackPost(req, res, parsed) {
260
+ const body = await readJsonOr400(req, res);
261
+ if (!body) return;
262
+ const provider = typeof body.provider === 'string' ? body.provider : '';
263
+ const state = typeof body.state === 'string' ? body.state : '';
264
+ const code = typeof body.code === 'string' ? body.code : '';
265
+ const errorParam = typeof body.error === 'string' ? body.error : '';
266
+ const result = await finishOAuth({ provider, state, code, errorParam, format: 'json' });
267
+ if (result.status === 200) {
268
+ return sendJSON(res, 200, { ok: true, provider, account: result.account });
269
+ }
270
+ return sendJSON(res, result.status, { ok: false, error: result.error, code: result.code });
271
+ }
272
+
273
+ module.exports = { handleAuth, handleOAuthCallback, handleOAuthCallbackPost };