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,327 @@
1
+ 'use strict';
2
+ // Per-project "hide file content" (redaction) rules.
3
+ //
4
+ // The user can mark line ranges of a specific source file that the agent
5
+ // file tools should not reveal, and can also select arbitrary text to hide
6
+ // by character range. This module owns the read side: it loads the rules
7
+ // from the project settings (<projectDir>/.mouaif.json or the DB-backed
8
+ // project row, via src/settings.js) and tells the file-tool runners which
9
+ // lines (and which characters within a line) of which file are hidden.
10
+ //
11
+ // The rules are stored on the project object under `hideFileContent`:
12
+ // [
13
+ // { path: 'src/index.js',
14
+ // ranges: [ { start: 12, end: 20 }, { start: 45, end: 45 } ], // whole lines
15
+ // chars: [ { startLine: 3, endLine: 3, startCol: 5, endCol: 11 } ] } // character spans
16
+ // ]
17
+ // `path` is a POSIX-relative path from the project root (the same value
18
+ // the file picker reports). `ranges` are 1-indexed, inclusive line spans;
19
+ // `chars` are 1-indexed, inclusive line & column spans. Both are optional
20
+ // per rule (older saved rules only have `ranges`), so existing data stays
21
+ // valid. A character span hides the selected characters on its boundary
22
+ // lines and the full lines between them.
23
+ //
24
+ // Scope: only the tools that *reveal* content are redacted — `read_file`
25
+ // and `search_files`. `list_files` lists paths (no content), and
26
+ // `write_file` / `edit_file` modify the on-disk file rather than exposing
27
+ // it, so they are not changed. The user-facing authorization preview card
28
+ // is also left intact: it is shown to the project owner who configured the
29
+ // redaction, not to the model.
30
+ //
31
+ // Everything here is best-effort and never throws: a missing project, a
32
+ // malformed rule, or a file that no longer exists must not crash a tool
33
+ // call. If the rules cannot be read, the tools behave exactly as if no
34
+ // redaction were configured.
35
+ const settings = require('./settings.js');
36
+ // Marker that replaces the body of a hidden line. Kept short so it does
37
+ // not dominate the model context, but distinct enough that the model can
38
+ // tell a line was intentionally withheld.
39
+ const REDACT_MARKER = '[hidden]';
40
+ // Return the stored redaction entries for a project. Always returns an
41
+ // array; any read problem yields [] so the callers can no-op.
42
+ function getEntries(projectDir) {
43
+ if (!projectDir || typeof projectDir !== 'string') return [];
44
+ let project;
45
+ try { project = settings.getProject(projectDir); }
46
+ catch { return []; }
47
+ const entries = project && project.hideFileContent;
48
+ return Array.isArray(entries) ? entries : [];
49
+ }
50
+ // Normalize a project file path to a POSIX-relative path with no leading
51
+ // './'. Mirrors src/tools/files.js toRelPath's output so rule paths match
52
+ // the rel paths the tools compute. Returns '' for anything non-string so a
53
+ // malformed rule can never match and hide the wrong file.
54
+ function normalizePath(p) {
55
+ if (typeof p !== 'string' || !p.trim()) return '';
56
+ // Strip a leading `./` BEFORE the slash collapsing, so a rule the user
57
+ // saved as `./src/a.js` (the editor reports paths with the prefix in some
58
+ // flows) lands on the same value src/tools/files.js toRelPath computes.
59
+ // Otherwise the rule is silently dead: it never matches the file it was
60
+ // written to hide. This was the previously-broken `while (s.startsWith`
61
+ // line, which ran before `s` was declared.
62
+ let s = p.trim().replace(/\\/g, '/');
63
+ while (s.startsWith('./')) s = s.slice(2);
64
+ s = s.replace(/\/+/g, '/').replace(/\/+$/, '');
65
+ return s;
66
+ }
67
+ // Sanitize a single character span to { startLine, endLine, startCol,
68
+ // endCol }. Columns are 1-indexed, inclusive character positions; lines
69
+ // are 1-indexed, inclusive. A span must be non-empty (the end position
70
+ // must be at or after the start) to be worth storing. Returns null for a
71
+ // malformed span so the caller can drop it.
72
+ function normalizeCharSpan(raw) {
73
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
74
+ const startLine = Number.isInteger(raw.startLine) ? raw.startLine : 0;
75
+ const endLine = Number.isInteger(raw.endLine) ? raw.endLine : 0;
76
+ const startCol = Number.isInteger(raw.startCol) ? raw.startCol : 0;
77
+ const endCol = Number.isInteger(raw.endCol) ? raw.endCol : 0;
78
+ // Validate line ordering first, then the column ordering for a
79
+ // single-line span (a multi-line span allows any column, since the
80
+ // middle lines are fully hidden and the boundary lines are partial).
81
+ if (startLine < 1 || endLine < startLine) return null;
82
+ if (startCol < 1 || endCol < startCol) {
83
+ // A multi-line span only needs its boundary columns to be at least 1.
84
+ if (startLine === endLine) return null;
85
+ if (startCol < 1 || endCol < 1) return null;
86
+ }
87
+ return { startLine, endLine, startCol, endCol };
88
+ }
89
+ // Sanitize a single rules entry to a { path, ranges, chars } shape. Invalid
90
+ // spans are dropped; valid spans are kept with start clamped to <= end.
91
+ // Returns null for a malformed entry (no path, or no spans at all) so the
92
+ // caller can skip it. Both `ranges` (line spans) and `chars` (character
93
+ // spans) are optional; a rule needs at least one of them to be worth
94
+ // storing.
95
+ function normalizeEntry(raw) {
96
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
97
+ const path = normalizePath(raw.path);
98
+ if (!path) return null;
99
+ const ranges = Array.isArray(raw.ranges)
100
+ ? raw.ranges.filter((r) => r && typeof r === 'object').map((r) => {
101
+ const start = Number.isInteger(r.start) ? r.start : 0;
102
+ const end = Number.isInteger(r.end) ? r.end : 0;
103
+ if (start < 1 || end < start) return null;
104
+ return { start, end };
105
+ }).filter(Boolean)
106
+ : [];
107
+ const chars = Array.isArray(raw.chars)
108
+ ? raw.chars.filter((c) => c && typeof c === 'object').map(normalizeCharSpan).filter(Boolean)
109
+ : [];
110
+ if (!ranges.length && !chars.length) return null;
111
+ // Keep the serialized shape clean and backward-compatible: only include
112
+ // `chars` when there are character spans, so a line-only rule stays
113
+ // `{ path, ranges }` exactly as it was before character ranges existed.
114
+ const entry = { path, ranges };
115
+ if (chars.length) entry.chars = chars;
116
+ return entry;
117
+ }
118
+ // Are `line` (1-indexed) and any other line within a hidden range of the
119
+ // given rule? `line` is a single 1-indexed line number.
120
+ function isLineHidden(rule, line) {
121
+ if (!rule || !rule.ranges || !Number.isInteger(line) || line < 1) return false;
122
+ return rule.ranges.some((r) => line >= r.start && line <= r.end);
123
+ }
124
+ // Apply the character spans of `rule` to a single line of text and return
125
+ // the redacted line (or the original). `lineNumber` is the 1-indexed line
126
+ // in the file; the whole-line `hidden` flag is also honoured. Character
127
+ // spans on a line are redacted in place: the selected characters are
128
+ // replaced by the marker. A single-line span hides [startCol, endCol]; a
129
+ // multi-line span hides from startCol on the first line, the full middle
130
+ // lines, and up to endCol on the last. Returns { text, redacted }.
131
+ function redactLine(rule, lineNumber, line, hidden) {
132
+ let text = line;
133
+ let redacted = false;
134
+ if (hidden) return { text: REDACT_MARKER, redacted: true };
135
+ const spans = rule && rule.chars ? rule.chars : [];
136
+ const selected = spans.filter((s) => lineNumber >= s.startLine && lineNumber <= s.endLine);
137
+ if (!selected.length) return { text, redacted };
138
+ const chars = Array.from(text);
139
+ const length = chars.length;
140
+ // Collect the absolute [start, end) character range to hide. We clamp to
141
+ // the line length so a stale span (line shrunk since it was saved) never
142
+ // throws and never hides beyond the line. Suffixes / prefixes that fall
143
+ // outside the file are simply not hidden.
144
+ const range = selected.reduce((acc, s) => {
145
+ let from, to;
146
+ if (s.startLine === s.endLine) {
147
+ from = s.startCol - 1;
148
+ to = s.endCol;
149
+ } else if (lineNumber === s.startLine) {
150
+ from = s.startCol - 1;
151
+ to = length;
152
+ } else if (lineNumber === s.endLine) {
153
+ from = 0;
154
+ to = s.endCol;
155
+ } else {
156
+ from = 0;
157
+ to = length;
158
+ }
159
+ if (from < 0) from = 0;
160
+ if (to > length) to = length;
161
+ if (to < from) to = from;
162
+ if (from < acc.from) acc.from = from;
163
+ if (to > acc.to) acc.to = to;
164
+ return acc;
165
+ }, { from: length, to: 0 });
166
+ if (range.to <= range.from) return { text, redacted };
167
+ const before = chars.slice(0, range.from).join('');
168
+ const after = chars.slice(range.to).join('');
169
+ text = before + REDACT_MARKER + after;
170
+ redacted = text !== line;
171
+ return { text, redacted };
172
+ }
173
+ // Replace every hidden line in `text` with the REDACT_MARKER, keeping the
174
+ // newline structure intact so line numbers and the total line count stay
175
+ // identical to the on-disk file. Whole-line ranges collapse to the marker;
176
+ // character spans redact the selected characters in place. Returns
177
+ // { text, redacted, hiddenLines }. `redacted` is true when at least one
178
+ // line was changed; `hiddenLines` is the count of lines that were fully
179
+ // hidden. A rule that no longer matches the file leaves the text
180
+ // untouched. `startLine` is the original 1-indexed line of the first line
181
+ // in a read_file slice.
182
+ function redactText(projectDir, relPath, text, startLine = 1) {
183
+ const rule = ruleForPath(projectDir, relPath);
184
+ if (!rule || typeof text !== 'string') {
185
+ return { text, redacted: false, hiddenLines: 0, rule };
186
+ }
187
+ let hiddenLines = 0;
188
+ let changed = false;
189
+ const lines = text.split('\n').map((line, idx) => {
190
+ const lineNumber = idx + startLine;
191
+ if (isLineHidden(rule, lineNumber)) {
192
+ hiddenLines++;
193
+ changed = true;
194
+ return REDACT_MARKER;
195
+ }
196
+ const out = redactLine(rule, lineNumber, line, false);
197
+ if (out.redacted) changed = true;
198
+ return out.text;
199
+ });
200
+ return { text: lines.join('\n'), redacted: changed, hiddenLines, rule };
201
+ }
202
+ // Build a normalized (read-only) rule list for a project. Also used by the
203
+ // settings UI to show a clean view of what is stored.
204
+ function getRules(projectDir) {
205
+ return getEntries(projectDir)
206
+ .map(normalizeEntry)
207
+ .filter(Boolean);
208
+ }
209
+ // Build one lookup index for a whole search: the normalized rules plus a
210
+ // Map from normalized path to rule, and a Map from normalized path to the
211
+ // rules that match it by path *suffix*.
212
+ //
213
+ // This exists because the redaction check used to call `getRules()` (and
214
+ // therefore re-read and re-parse the project settings) once per line of
215
+ // every scanned file — 27k settings reads for one 2-second search. Callers
216
+ // with many paths to test build the index once and hand it to
217
+ // `matchIsHiddenIn` / `lineIsHiddenIn`.
218
+ //
219
+ // The suffix map is the safety net: the engine that produces the paths can
220
+ // report them differently from the rule (`./src/a.js` vs `src/a.js`,
221
+ // absolute vs relative) and a rule that fails to match is a leak, not a
222
+ // cosmetic bug. So a rule also applies to any path that ends with the rule
223
+ // path on a `/` boundary.
224
+ function buildRuleIndex(projectDir) {
225
+ const rules = getRules(projectDir);
226
+ const byPath = new Map();
227
+ const bySuffix = new Map();
228
+ for (const rule of rules) {
229
+ byPath.set(rule.path, rule);
230
+ const parts = rule.path.split('/');
231
+ for (let i = 1; i < parts.length; i++) {
232
+ const suffix = parts.slice(i).join('/');
233
+ if (!bySuffix.has(suffix)) bySuffix.set(suffix, []);
234
+ bySuffix.get(suffix).push(rule);
235
+ }
236
+ }
237
+ return { rules, byPath, bySuffix };
238
+ }
239
+ // Resolve the rules that apply to one path against a prebuilt index.
240
+ // Exact normalized match first, then the suffix map. Returns [] when
241
+ // nothing applies. Never throws.
242
+ function rulesForPathIn(index, relPath) {
243
+ if (!index || !relPath) return [];
244
+ const target = normalizePath(relPath);
245
+ if (!target) return [];
246
+ const exact = index.byPath.get(target);
247
+ const suffix = index.bySuffix.get(target);
248
+ if (exact && suffix) return [exact].concat(suffix.filter((r) => r !== exact));
249
+ if (exact) return [exact];
250
+ return suffix || [];
251
+ }
252
+ // Return the { path, ranges, chars } rule for a given project file, or null.
253
+ function ruleForPath(projectDir, relPath) {
254
+ const target = normalizePath(relPath);
255
+ if (!target) return null;
256
+ for (const rule of getRules(projectDir)) {
257
+ if (rule.path === target) return rule;
258
+ }
259
+ return null;
260
+ }
261
+ // Predicate over a prebuilt rule index: true when the given 1-indexed line
262
+ // is fully hidden by any rule that applies to `relPath`. The no-index
263
+ // `lineIsHidden` wrapper below keeps the single-path callers (and tests)
264
+ // working.
265
+ function lineIsHiddenIn(index, relPath, line) {
266
+ if (!Number.isInteger(line) || line < 1) return false;
267
+ for (const rule of rulesForPathIn(index, relPath)) {
268
+ if (isLineHidden(rule, line)) return true;
269
+ }
270
+ return false;
271
+ }
272
+ // Same, for a match that occupies columns [colStart, colEnd] on the line.
273
+ // The whole line, or an overlapping character span, suppresses the match.
274
+ function matchIsHiddenIn(index, relPath, line, colStart, colEnd) {
275
+ if (!Number.isInteger(line) || line < 1) return false;
276
+ for (const rule of rulesForPathIn(index, relPath)) {
277
+ if (isLineHidden(rule, line)) return true;
278
+ const spans = rule.chars || [];
279
+ for (const s of spans) {
280
+ if (line < s.startLine || line > s.endLine) continue;
281
+ if (typeof colStart !== 'number' || typeof colEnd !== 'number') return true;
282
+ const startCol = line === s.startLine ? s.startCol : 1;
283
+ const endCol = line === s.endLine ? s.endCol : Number.MAX_SAFE_INTEGER;
284
+ if (Math.max(startCol, colStart) <= Math.min(endCol, colEnd)) return true;
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+ // A predicate used by callers that only have a project dir and a path (the
290
+ // search engine, when it did not bother to build an index). Returns true
291
+ // when the given 1-indexed line of the given file is hidden and should
292
+ // therefore be excluded from any match list. `colStart`/`colEnd` are
293
+ // 1-indexed inclusive character columns occupied by the match on that line;
294
+ // when omitted the whole line is treated as potentially hidden so a search
295
+ // result is conservatively suppressed. Never throws.
296
+ function matchIsHidden(projectDir, relPath, line, colStart, colEnd) {
297
+ return matchIsHiddenIn(buildRuleIndex(projectDir), relPath, line, colStart, colEnd);
298
+ }
299
+ // Convenience predicate for callers that only care about whole lines.
300
+ function lineIsHidden(projectDir, relPath, line) {
301
+ return lineIsHiddenIn(buildRuleIndex(projectDir), relPath, line);
302
+ }
303
+ // The rule that applies to a path, resolved through the same exact-then-
304
+ // suffix lookup the search engine uses, so a one-path caller and the
305
+ // engine never disagree about whether a file is covered.
306
+ function ruleForPathIn(index, relPath) {
307
+ const matches = rulesForPathIn(index, relPath);
308
+ return matches.length ? matches[0] : null;
309
+ }
310
+ module.exports = {
311
+ REDACT_MARKER,
312
+ getEntries,
313
+ getRules,
314
+ buildRuleIndex,
315
+ rulesForPathIn,
316
+ ruleForPath,
317
+ ruleForPathIn,
318
+ isLineHidden,
319
+ redactText,
320
+ lineIsHidden,
321
+ lineIsHiddenIn,
322
+ matchIsHidden,
323
+ matchIsHiddenIn,
324
+ normalizePath,
325
+ normalizeEntry,
326
+ normalizeCharSpan
327
+ };