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/projects.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Project / folder picker — files API + registered-project store.
|
|
4
|
+
//
|
|
5
|
+
// Implements docs/decisions.md section 4 (full filesystem browse) and the
|
|
6
|
+
// "new project opens a folder list + create new folder" rule from
|
|
7
|
+
// .github/copilot-instructions.md.
|
|
8
|
+
//
|
|
9
|
+
// Two surfaces:
|
|
10
|
+
//
|
|
11
|
+
// Filesystem surface (the picker):
|
|
12
|
+
// listDir(absPath) -> { dir, entries: [{ name, path, hasChildren }] }
|
|
13
|
+
// createDir(absPath) -> { path }
|
|
14
|
+
//
|
|
15
|
+
// Registered-project surface (the user's chosen projects):
|
|
16
|
+
// listProjects() -> [{ id, path, name, createdAt }]
|
|
17
|
+
// getProject(id) -> { id, path, name, createdAt } | null
|
|
18
|
+
// registerProject(path) -> { id, path, name, createdAt }
|
|
19
|
+
// removeProject(id) -> true | false
|
|
20
|
+
//
|
|
21
|
+
// Registered projects are stored in the app settings under the `projects`
|
|
22
|
+
// key. They survive restarts. Removing a registered project does NOT
|
|
23
|
+
// delete the folder on disk.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
const settings = require('./settings.js');
|
|
30
|
+
|
|
31
|
+
const ALLOW_ANY_ROOT = process.env.MOUAIF_ALLOW_ANY_ROOT === '1';
|
|
32
|
+
|
|
33
|
+
// ---- Path safety --------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function isAbsolutePath(p) {
|
|
36
|
+
return typeof p === 'string' && path.isAbsolute(p);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isUnderHome(absPath) {
|
|
40
|
+
const home = os.homedir();
|
|
41
|
+
const resolved = path.resolve(absPath);
|
|
42
|
+
const homeResolved = path.resolve(home);
|
|
43
|
+
// Same path, or a descendant.
|
|
44
|
+
if (resolved === homeResolved) return true;
|
|
45
|
+
const rel = path.relative(homeResolved, resolved);
|
|
46
|
+
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureSafeRoot(absPath) {
|
|
50
|
+
if (!isAbsolutePath(absPath)) {
|
|
51
|
+
const e = new Error('Path must be absolute');
|
|
52
|
+
e.code = 'EBADPATH';
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
if (!ALLOW_ANY_ROOT && !isUnderHome(absPath)) {
|
|
56
|
+
const e = new Error('Path must be under the user home. Set MOUAIF_ALLOW_ANY_ROOT=1 to opt out.');
|
|
57
|
+
e.code = 'EOUTSIDE_HOME';
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
return path.resolve(absPath);
|
|
61
|
+
}
|
|
62
|
+
// The top-most directory the file editor / picker may browse to. With
|
|
63
|
+
// MOUAIF_ALLOW_ANY_ROOT the whole filesystem is reachable so the boundary
|
|
64
|
+
// is the root path; otherwise it is the user's home. Used by the frontend
|
|
65
|
+
// to clamp the Up button at the natural top so it never asks the server
|
|
66
|
+
// for a directory that would fail the home guard.
|
|
67
|
+
function browseTop(absPath) {
|
|
68
|
+
if (!absPath || !isAbsolutePath(absPath)) return null;
|
|
69
|
+
if (ALLOW_ANY_ROOT) {
|
|
70
|
+
const root = path.parse(path.resolve(absPath)).root;
|
|
71
|
+
return root;
|
|
72
|
+
}
|
|
73
|
+
return os.homedir();
|
|
74
|
+
}
|
|
75
|
+
// True when `absPath` sits at or above the browse boundary (home, or /)
|
|
76
|
+
// for the given home/ALLOW key — i.e. there is no parent to go up to.
|
|
77
|
+
function isBrowseTop(absPath) {
|
|
78
|
+
const top = browseTop(absPath);
|
|
79
|
+
if (!top) return true;
|
|
80
|
+
const resolved = path.resolve(absPath || '');
|
|
81
|
+
const topResolved = path.resolve(top);
|
|
82
|
+
return resolved === topResolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- Filesystem surface -------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function listDir(absPath) {
|
|
88
|
+
const safe = ensureSafeRoot(absPath);
|
|
89
|
+
let names;
|
|
90
|
+
try {
|
|
91
|
+
names = fs.readdirSync(safe);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Surface typed errors so the UI can show "permission denied" vs "no such dir".
|
|
94
|
+
const wrapped = new Error(e.message);
|
|
95
|
+
wrapped.code = e.code || 'EREAD';
|
|
96
|
+
wrapped.path = safe;
|
|
97
|
+
throw wrapped;
|
|
98
|
+
}
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
// Skip hidden by default — chat list / project picker doesn't need dotfiles.
|
|
102
|
+
if (name.startsWith('.')) continue;
|
|
103
|
+
const full = path.join(safe, name);
|
|
104
|
+
let stat;
|
|
105
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
106
|
+
if (!stat.isDirectory()) continue;
|
|
107
|
+
entries.push({
|
|
108
|
+
name,
|
|
109
|
+
path: full,
|
|
110
|
+
hasChildren: hasImmediateSubdirs(full)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Stable order: case-insensitive name.
|
|
114
|
+
entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
115
|
+
return { dir: safe, entries };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasImmediateSubdirs(absPath) {
|
|
119
|
+
let names;
|
|
120
|
+
try { names = fs.readdirSync(absPath); } catch { return false; }
|
|
121
|
+
for (const n of names) {
|
|
122
|
+
if (n.startsWith('.')) continue;
|
|
123
|
+
try {
|
|
124
|
+
const st = fs.statSync(path.join(absPath, n));
|
|
125
|
+
if (st.isDirectory()) return true;
|
|
126
|
+
} catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createDir(absPath) {
|
|
132
|
+
const safe = ensureSafeRoot(absPath);
|
|
133
|
+
try {
|
|
134
|
+
fs.mkdirSync(safe, { recursive: false });
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (e.code === 'EEXIST') {
|
|
137
|
+
const wrapped = new Error('Directory already exists');
|
|
138
|
+
wrapped.code = 'EEXIST';
|
|
139
|
+
wrapped.path = safe;
|
|
140
|
+
throw wrapped;
|
|
141
|
+
}
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
return { path: safe };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- Registered-project surface ----------------------------------------
|
|
148
|
+
|
|
149
|
+
function id() {
|
|
150
|
+
return crypto.randomBytes(8).toString('hex');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readProjects() {
|
|
154
|
+
const app = settings.getApp();
|
|
155
|
+
const list = Array.isArray(app.projects) ? app.projects : [];
|
|
156
|
+
// Defensive copy, validated.
|
|
157
|
+
return list.filter(p => p && typeof p.path === 'string' && typeof p.id === 'string');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeProjects(list) {
|
|
161
|
+
// Single shallow patch — settings.setApp does the merge.
|
|
162
|
+
settings.setApp({ projects: list });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function emptyTotalCost() {
|
|
166
|
+
return { total: 0, known: false, currency: 'USD', knownCount: 0 };
|
|
167
|
+
}
|
|
168
|
+
function normalizeTotalCost(value) {
|
|
169
|
+
if (!value || typeof value.total !== 'number' || !Number.isFinite(value.total)) return emptyTotalCost();
|
|
170
|
+
const knownCount = Number.isInteger(value.knownCount) && value.knownCount >= 0
|
|
171
|
+
? value.knownCount
|
|
172
|
+
: (value.known === true ? 1 : 0);
|
|
173
|
+
return {
|
|
174
|
+
total: Math.max(0, value.total),
|
|
175
|
+
known: knownCount > 0,
|
|
176
|
+
currency: 'USD',
|
|
177
|
+
knownCount
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function listProjects() {
|
|
181
|
+
return readProjects().map((project) => ({
|
|
182
|
+
...project,
|
|
183
|
+
totalCost: normalizeTotalCost(project.totalCost)
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
function setProjectTotalCost(projectDir, totalCost) {
|
|
187
|
+
const list = readProjects();
|
|
188
|
+
const idx = list.findIndex((project) => path.resolve(project.path) === path.resolve(projectDir));
|
|
189
|
+
if (idx < 0) return null;
|
|
190
|
+
const normalized = normalizeTotalCost(totalCost);
|
|
191
|
+
list[idx] = { ...list[idx], totalCost: normalized };
|
|
192
|
+
writeProjects(list);
|
|
193
|
+
return normalized;
|
|
194
|
+
}
|
|
195
|
+
function adjustProjectTotalCost(projectDir, totalDelta, knownCountDelta) {
|
|
196
|
+
const list = readProjects();
|
|
197
|
+
const idx = list.findIndex((project) => path.resolve(project.path) === path.resolve(projectDir));
|
|
198
|
+
if (idx < 0) return null;
|
|
199
|
+
const current = normalizeTotalCost(list[idx].totalCost);
|
|
200
|
+
const knownCount = Math.max(0, current.knownCount + knownCountDelta);
|
|
201
|
+
const totalCost = {
|
|
202
|
+
total: knownCount > 0 ? Math.max(0, current.total + totalDelta) : 0,
|
|
203
|
+
known: knownCount > 0,
|
|
204
|
+
currency: 'USD',
|
|
205
|
+
knownCount
|
|
206
|
+
};
|
|
207
|
+
list[idx] = { ...list[idx], totalCost };
|
|
208
|
+
writeProjects(list);
|
|
209
|
+
return totalCost;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getProject(pid) {
|
|
213
|
+
return readProjects().find(p => p.id === pid) || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function registerProject(absPath) {
|
|
217
|
+
const safe = ensureSafeRoot(absPath);
|
|
218
|
+
// The folder must exist. Use stat, not existsSync, to get a real error
|
|
219
|
+
// for ENOTDIR / EACCES.
|
|
220
|
+
const stat = fs.statSync(safe);
|
|
221
|
+
if (!stat.isDirectory()) {
|
|
222
|
+
const e = new Error('Path is not a directory');
|
|
223
|
+
e.code = 'ENOTDIR';
|
|
224
|
+
e.path = safe;
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
const existing = readProjects();
|
|
228
|
+
// Dedupe by canonical path.
|
|
229
|
+
const dupe = existing.find(p => path.resolve(p.path) === safe);
|
|
230
|
+
if (dupe) return dupe;
|
|
231
|
+
const row = {
|
|
232
|
+
id: id(),
|
|
233
|
+
path: safe,
|
|
234
|
+
name: path.basename(safe),
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
totalCost: emptyTotalCost()
|
|
237
|
+
};
|
|
238
|
+
writeProjects([...existing, row]);
|
|
239
|
+
return row;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function removeProject(pid) {
|
|
243
|
+
const before = readProjects();
|
|
244
|
+
const after = before.filter(p => p.id !== pid);
|
|
245
|
+
if (after.length === before.length) return false;
|
|
246
|
+
writeProjects(after);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Rename a registered project's display label. The on-disk folder
|
|
251
|
+
// is not touched. Trims and validates the new name; returns null if
|
|
252
|
+
// the project doesn't exist or the name is empty.
|
|
253
|
+
function renameProject(pid, newName) {
|
|
254
|
+
if (!pid || typeof pid !== 'string') return null;
|
|
255
|
+
if (typeof newName !== 'string') return null;
|
|
256
|
+
const trimmed = newName.trim();
|
|
257
|
+
if (!trimmed) return null;
|
|
258
|
+
const list = readProjects();
|
|
259
|
+
const idx = list.findIndex(p => p.id === pid);
|
|
260
|
+
if (idx < 0) return null;
|
|
261
|
+
const updated = Object.assign({}, list[idx], { name: trimmed });
|
|
262
|
+
list[idx] = updated;
|
|
263
|
+
writeProjects(list);
|
|
264
|
+
return updated;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
// filesystem
|
|
269
|
+
listDir,
|
|
270
|
+
createDir,
|
|
271
|
+
// registered projects
|
|
272
|
+
listProjects,
|
|
273
|
+
getProject,
|
|
274
|
+
registerProject,
|
|
275
|
+
removeProject,
|
|
276
|
+
renameProject,
|
|
277
|
+
setProjectTotalCost,
|
|
278
|
+
adjustProjectTotalCost,
|
|
279
|
+
// helpers (exported for tests + future inspector)
|
|
280
|
+
isUnderHome,
|
|
281
|
+
ensureSafeRoot,
|
|
282
|
+
browseTop,
|
|
283
|
+
isBrowseTop,
|
|
284
|
+
ALLOW_ANY_ROOT
|
|
285
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Prompt-size profiles.
|
|
4
|
+
//
|
|
5
|
+
// Implements the "Three prompt-size profiles" feature from
|
|
6
|
+
// .github/copilot-instructions.md §4. The chat record carries a
|
|
7
|
+
// `promptSize` field ('very-small' | 'average' | 'extensive') and
|
|
8
|
+
// `handleChatStream` in src/index.js prepends the matching profile's
|
|
9
|
+
// system message to the upstream `messages` array, immediately before
|
|
10
|
+
// any custom-prompt block.
|
|
11
|
+
//
|
|
12
|
+
// Resolution order (decisions §2): defaults -> app -> project.
|
|
13
|
+
// - chat.promptSize (per-chat override; see src/chats.js) wins if set
|
|
14
|
+
// - else settings.getResolved(projectDir).promptSize
|
|
15
|
+
// - else 'average' (the DEFAULTS floor in src/settings.js)
|
|
16
|
+
//
|
|
17
|
+
// Profiles share a provider-neutral core: changing the size adds detail,
|
|
18
|
+
// not conflicting rules about autonomy, safety, tools, or progress.
|
|
19
|
+
// Tool declarations are sized separately by reduceToolSpecs below.
|
|
20
|
+
//
|
|
21
|
+
// Profiles:
|
|
22
|
+
// - very-small : compact core + on-demand tool-schema discovery.
|
|
23
|
+
// - average : the same core + an explicit inspect/edit/verify loop.
|
|
24
|
+
// The recommended default, with full tool schemas.
|
|
25
|
+
// - extensive : all average guidance + planning, verification, and
|
|
26
|
+
// concrete workflow examples. Also uses full tool schemas.
|
|
27
|
+
//
|
|
28
|
+
// Public surface:
|
|
29
|
+
// PROFILES : { 'very-small', 'average', 'extensive' }
|
|
30
|
+
// DEFAULT_PROFILE : 'average'
|
|
31
|
+
// isValidProfile(value) : boolean
|
|
32
|
+
// profileSystemMessage(value) : string
|
|
33
|
+
// listProfiles() : [{ id, label, description, summary }, ...]
|
|
34
|
+
// resolveProfile({ chat, projectDir }): { id, label, description, summary, systemMessage }
|
|
35
|
+
// describeProfile(value) : { id, label, description, summary, systemMessage }
|
|
36
|
+
// or null if value is unknown
|
|
37
|
+
|
|
38
|
+
const CORE_GUIDANCE = [
|
|
39
|
+
'You are a coding assistant running inside mouaif, a mobile chat UI.',
|
|
40
|
+
'',
|
|
41
|
+
'- Be concise by default. Use short Markdown, language-tagged code fences, and project-relative paths.',
|
|
42
|
+
'- Follow applicable project and custom instructions, respecting higher-priority instructions.',
|
|
43
|
+
'- Make reasonable, reversible decisions; state assumptions briefly. Ask when ambiguity affects scope, safety, or correctness.',
|
|
44
|
+
'- Ask before destructive actions, full-file rewrites, dependency installs, or pushes unless explicitly authorized. Preserve unrelated user work.',
|
|
45
|
+
'- Use only enabled tools and respect authorization gates. If a tool schema is missing, use discover_tool before calling it.',
|
|
46
|
+
'- Inspect before editing; make focused changes, run relevant checks, and fix introduced failures. Shell has no stdin: use non-interactive commands, never a REPL.',
|
|
47
|
+
'- If report_progress is enabled, call it at the start of every task with status: "running" and at completion with status: "completed" and current equal to total. Use status: "failed" if blocked; never mark unfinished work completed.',
|
|
48
|
+
'- Never invent project facts or test results. Finish with the outcome, checks run, and any limitations. Do not expose secrets.'
|
|
49
|
+
].join('\n');
|
|
50
|
+
const WORKFLOW_GUIDANCE = [
|
|
51
|
+
'Working in the project:',
|
|
52
|
+
'- Inspect relevant files and project instructions using available search/read tools before proposing a fix. Ask for missing context only when tools cannot retrieve it.',
|
|
53
|
+
'- For implementation requests, apply the change when authorized tools are available; do not stop at a proposed diff. For questions or reviews, answer without changing files unless asked.',
|
|
54
|
+
'- Read the relevant region, then use edit_file with an exact, unique oldText/newText block. Use write_file for new files or explicitly authorized full rewrites. If an edit fails to match, re-read before retrying.',
|
|
55
|
+
'- Keep changes scoped to the request and existing conventions. Avoid unrelated refactors, dependency changes, or reverting user edits.',
|
|
56
|
+
'- Run targeted tests and the project lint/build as appropriate. Read failures, fix issues caused by the change, and repeat until verified, blocked, or cancelled. State clearly when checks could not run or failures are unrelated.',
|
|
57
|
+
'- Keep progress updates brief and tied to real milestones. Use { title, current, total, status, message } with a stable title, a positive total, and current between zero and total. If the tool is unavailable, use a short plain-language update instead.',
|
|
58
|
+
'- In the final reply, summarize what changed and what was verified; cite relevant paths and note remaining work. Do not dump raw tool output or claim success without evidence.'
|
|
59
|
+
].join('\n');
|
|
60
|
+
const EXTENSIVE_GUIDANCE = [
|
|
61
|
+
'Planning and verification:',
|
|
62
|
+
'- For multi-step work, outline a short plan and revise it when evidence changes. Use task tracking if available and useful; skip elaborate plans for simple requests.',
|
|
63
|
+
'- Inspect callers, nearby tests, and configuration to understand the behavior before editing. Prefer the smallest fix that addresses the root cause.',
|
|
64
|
+
'- Add or update regression tests for changed behavior and update related documentation. Test error paths and edge cases as well as the happy path.',
|
|
65
|
+
'- For UI changes, check narrow mobile widths first, touch targets, keyboard access, and loading/error states; then check larger screens.',
|
|
66
|
+
'- Parallelize independent reads or checks when useful. Keep dependent edits and commands ordered, and avoid concurrent writes to the same files.',
|
|
67
|
+
'- Review the final diff for accidental changes and sensitive data. Summarize relevant tool evidence without copying credentials, tokens, or unnecessary private data into the transcript.',
|
|
68
|
+
'',
|
|
69
|
+
'Workflow examples:',
|
|
70
|
+
'- Bug fix: reproduce or inspect the failing path, add a focused regression test, make the smallest correction, rerun checks, and report the results.',
|
|
71
|
+
'- Harmless ambiguity: follow a nearby naming convention and mention the assumption. Material ambiguity: ask which behavior is intended before changing it.',
|
|
72
|
+
'- Blocked check: if tests require an unavailable service, report which checks ran and which could not; do not describe the feature as fully verified or mark the task completed while required work remains.'
|
|
73
|
+
].join('\n');
|
|
74
|
+
const AVERAGE_GUIDANCE = CORE_GUIDANCE + '\n\n' + WORKFLOW_GUIDANCE;
|
|
75
|
+
const PROFILES = Object.freeze({
|
|
76
|
+
'very-small': {
|
|
77
|
+
id: 'very-small',
|
|
78
|
+
label: 'Very small',
|
|
79
|
+
description: 'Core coding rules with on-demand tool schemas. Smallest prompt.',
|
|
80
|
+
summary: 'core rules + compact tools',
|
|
81
|
+
systemMessage: CORE_GUIDANCE
|
|
82
|
+
},
|
|
83
|
+
'average': {
|
|
84
|
+
id: 'average',
|
|
85
|
+
label: 'Average',
|
|
86
|
+
description: 'Core rules + inspect, edit, and verify workflow. The recommended default.',
|
|
87
|
+
summary: 'core rules + tool workflow',
|
|
88
|
+
systemMessage: AVERAGE_GUIDANCE
|
|
89
|
+
},
|
|
90
|
+
'extensive': {
|
|
91
|
+
id: 'extensive',
|
|
92
|
+
label: 'Extensive',
|
|
93
|
+
description: 'All Average guidance + planning, verification, and workflow examples.',
|
|
94
|
+
summary: 'core rules + workflow + examples',
|
|
95
|
+
systemMessage: AVERAGE_GUIDANCE + '\n\n' + EXTENSIVE_GUIDANCE
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const DEFAULT_PROFILE = 'average';
|
|
100
|
+
|
|
101
|
+
function isValidProfile(value) {
|
|
102
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(PROFILES, value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function profileSystemMessage(value) {
|
|
106
|
+
if (!isValidProfile(value)) return PROFILES[DEFAULT_PROFILE].systemMessage;
|
|
107
|
+
return PROFILES[value].systemMessage;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeProfile(value) {
|
|
111
|
+
if (!isValidProfile(value)) return null;
|
|
112
|
+
// Return a shallow copy so the caller cannot mutate the frozen module-level data.
|
|
113
|
+
return Object.assign({}, PROFILES[value]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function listProfiles() {
|
|
117
|
+
return Object.keys(PROFILES).map(k => Object.assign({}, PROFILES[k]));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const DISCOVER_TOOL_NAME = 'discover_tool';
|
|
121
|
+
|
|
122
|
+
function cloneJson(value) {
|
|
123
|
+
if (value === undefined) return undefined;
|
|
124
|
+
return JSON.parse(JSON.stringify(value));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function cloneToolSpec(spec) {
|
|
128
|
+
const fn = (spec && spec.function) || {};
|
|
129
|
+
return {
|
|
130
|
+
type: 'function',
|
|
131
|
+
function: {
|
|
132
|
+
name: fn.name,
|
|
133
|
+
description: fn.description,
|
|
134
|
+
parameters: cloneJson(fn.parameters)
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toolNameList(specs) {
|
|
140
|
+
return (Array.isArray(specs) ? specs : [])
|
|
141
|
+
.map(s => s && s.function && s.function.name)
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function makeDiscoverToolSpec(specs) {
|
|
146
|
+
const names = toolNameList(specs);
|
|
147
|
+
const list = names.length ? names.join(', ') : '(none)';
|
|
148
|
+
return {
|
|
149
|
+
type: 'function',
|
|
150
|
+
function: {
|
|
151
|
+
name: DISCOVER_TOOL_NAME,
|
|
152
|
+
description: 'Discover the complete description and parameter schema for one available tool before calling it. Available tools: ' + list + '.',
|
|
153
|
+
parameters: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
toolName: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Name of the tool to expand.',
|
|
159
|
+
enum: names
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
required: ['toolName'],
|
|
163
|
+
additionalProperties: false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// reduceToolSpecs(specs, profileValue, opts) — shrink the tool declaration
|
|
170
|
+
// advertised to the model according to the active prompt-size profile.
|
|
171
|
+
// This is the core of the "three prompt-size profiles" feature
|
|
172
|
+
// (.github/copilot-instructions.md §4): the profile controls HOW MUCH
|
|
173
|
+
// of each tool is sent upstream, not just the system prompt text.
|
|
174
|
+
//
|
|
175
|
+
// very-small : compact tool list, name + short description only (no
|
|
176
|
+
// parameter schemas), plus discover_tool for on-demand
|
|
177
|
+
// full-schema expansion. The list is FIXED for the whole
|
|
178
|
+
// turn: the same tools are advertised on the first request
|
|
179
|
+
// and on every tool-loop follow-up. Growing the list after
|
|
180
|
+
// a discover_tool call would change the Anthropic cached
|
|
181
|
+
// prefix (system + tools) between requests, so the warm
|
|
182
|
+
// cache would be invalidated on every round and caching
|
|
183
|
+
// would never engage.
|
|
184
|
+
// average : full tool list, name + full description + parameters
|
|
185
|
+
// (the compact-but-complete default).
|
|
186
|
+
// extensive : same as average (full), kept separate so the extensive
|
|
187
|
+
// system prompt's best-practice guidance is what makes it
|
|
188
|
+
// "extensive"; the tools themselves are already complete.
|
|
189
|
+
//
|
|
190
|
+
// `specs` is an array of OpenAI-shape tool specs
|
|
191
|
+
// ({ type:'function', function:{ name, description, parameters } }).
|
|
192
|
+
// Returns a NEW array; the input is never mutated. Unknown profiles
|
|
193
|
+
// fall through to the full list.
|
|
194
|
+
function reduceToolSpecs(specs, profileValue, opts) {
|
|
195
|
+
if (!Array.isArray(specs) || !specs.length) return [];
|
|
196
|
+
const profile = isValidProfile(profileValue) ? profileValue : DEFAULT_PROFILE;
|
|
197
|
+
if (profile !== 'very-small') {
|
|
198
|
+
// average + extensive: full specs, defensively copied.
|
|
199
|
+
return specs.map(cloneToolSpec);
|
|
200
|
+
}
|
|
201
|
+
// very-small: discover_tool + one compact entry per tool, stable for
|
|
202
|
+
// the whole turn. Compact entries keep the token budget low (the
|
|
203
|
+
// profile's whole point) while the byte-stable list keeps the
|
|
204
|
+
// Anthropic cache prefix identical across tool-loop requests.
|
|
205
|
+
const out = [makeDiscoverToolSpec(specs)];
|
|
206
|
+
for (const spec of specs) {
|
|
207
|
+
const fn = (spec && spec.function) || {};
|
|
208
|
+
if (!fn.name) continue;
|
|
209
|
+
out.push({
|
|
210
|
+
type: 'function',
|
|
211
|
+
function: {
|
|
212
|
+
name: fn.name,
|
|
213
|
+
description: typeof fn.description === 'string' ? fn.description : '',
|
|
214
|
+
// No `parameters` block: the model gets the tool's shape via
|
|
215
|
+
// discover_tool if it needs it. Omitting it here is what keeps
|
|
216
|
+
// very-small's footprint small AND the prefix stable.
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// resolveProfile({ chat, projectDir }) — pick the profile that applies
|
|
224
|
+
// to a chat right now. Resolution: chat.promptSize (if valid) -> the
|
|
225
|
+
// project's resolved promptSize (defaults -> app -> project, per
|
|
226
|
+
// decisions §2) -> 'average'. The function never throws; an unknown
|
|
227
|
+
// value falls through to the next layer so a stale or hand-edited
|
|
228
|
+
// project file still gets a sensible prompt.
|
|
229
|
+
function resolveProfile(opts) {
|
|
230
|
+
const chat = opts && opts.chat;
|
|
231
|
+
const projectDir = opts && opts.projectDir;
|
|
232
|
+
let chosen = null;
|
|
233
|
+
if (chat && isValidProfile(chat.promptSize)) chosen = chat.promptSize;
|
|
234
|
+
if (!chosen && projectDir) {
|
|
235
|
+
try {
|
|
236
|
+
const settings = require('./settings.js');
|
|
237
|
+
const resolved = settings.getResolved(projectDir);
|
|
238
|
+
if (isValidProfile(resolved && resolved.promptSize)) chosen = resolved.promptSize;
|
|
239
|
+
} catch { /* fall through to default */ }
|
|
240
|
+
}
|
|
241
|
+
if (!chosen) chosen = DEFAULT_PROFILE;
|
|
242
|
+
return Object.assign({}, PROFILES[chosen], { systemMessage: PROFILES[chosen].systemMessage });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
PROFILES,
|
|
247
|
+
DEFAULT_PROFILE,
|
|
248
|
+
isValidProfile,
|
|
249
|
+
profileSystemMessage,
|
|
250
|
+
describeProfile,
|
|
251
|
+
listProfiles,
|
|
252
|
+
resolveProfile,
|
|
253
|
+
reduceToolSpecs,
|
|
254
|
+
DISCOVER_TOOL_NAME,
|
|
255
|
+
makeDiscoverToolSpec
|
|
256
|
+
};
|