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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Agent features — dynamic system context that tells the model which
|
|
4
|
+
// mouaif features are enabled/disabled for the current project and
|
|
5
|
+
// chat session. The model receives a terse summary injected into the
|
|
6
|
+
// upstream message array. A companion tool (`list_features`) lets the
|
|
7
|
+
// model query the full structured feature state at any time, which is
|
|
8
|
+
// especially useful in `very-small` prompt mode.
|
|
9
|
+
//
|
|
10
|
+
// Public surface:
|
|
11
|
+
// buildFeatureSummary({ chat, projectDir, project, authz, mcpServers })
|
|
12
|
+
// -> string (system-message content) | null
|
|
13
|
+
// LIST_FEATURES_SPEC : tool spec for the list_features tool
|
|
14
|
+
// dispatchListFeatures(...) : execute the tool call
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const TOOL_NAME = 'list_features';
|
|
19
|
+
|
|
20
|
+
const LIST_FEATURES_SPEC = {
|
|
21
|
+
type: 'function',
|
|
22
|
+
function: {
|
|
23
|
+
name: TOOL_NAME,
|
|
24
|
+
description: 'Describe every mouaif feature available in this project and their current on/off/ask state. Returns a JSON object with one key per feature family; each value contains the enable state, authorization mode, and a short description.',
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// buildFeatureSummary({ chat, projectDir, project, authz, mcpServers })
|
|
34
|
+
//
|
|
35
|
+
// Compose a short system message describing which features are
|
|
36
|
+
// available, enabled, and authorized. Returns null when no features
|
|
37
|
+
// are relevant (to avoid injecting an empty message).
|
|
38
|
+
function buildFeatureSummary(opts) {
|
|
39
|
+
const { chat, projectDir, project, authz, mcpServers } = opts || {};
|
|
40
|
+
const lines = [];
|
|
41
|
+
const feat = [];
|
|
42
|
+
|
|
43
|
+
// --- Built-in tools ---
|
|
44
|
+
// shell
|
|
45
|
+
if (authz && authz.tools && authz.tools.shell) {
|
|
46
|
+
const s = authz.tools.shell;
|
|
47
|
+
if (s.mode !== 'off') {
|
|
48
|
+
feat.push('[shell](off|ask|allow) → ' + s.mode + (s.mode === 'allowlist' ? ' (' + (Array.isArray(s.allowlist) ? s.allowlist.length : 0) + ' patterns)' : ''));
|
|
49
|
+
// Let the model know which shell dialect runs its commands
|
|
50
|
+
// (cmd.exe vs a POSIX sh) so it picks the right syntax.
|
|
51
|
+
try {
|
|
52
|
+
const shellMod = require('./tools/shell.js');
|
|
53
|
+
if (typeof shellMod.shellDialectHint === 'function') {
|
|
54
|
+
feat.push('[shell] ' + shellMod.shellDialectHint());
|
|
55
|
+
}
|
|
56
|
+
} catch { /* label is best-effort */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// subagent
|
|
60
|
+
if (authz && authz.tools && authz.tools.subagent) {
|
|
61
|
+
const s = authz.tools.subagent;
|
|
62
|
+
if (s.mode !== 'off') {
|
|
63
|
+
feat.push('[subagent](off|ask|allow) → ' + s.mode);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// progress updates
|
|
67
|
+
if (authz && authz.tools && authz.tools.report_progress) {
|
|
68
|
+
const s = authz.tools.report_progress;
|
|
69
|
+
if (s.mode !== 'off') {
|
|
70
|
+
feat.push('[report_progress](off|ask|allow) → ' + s.mode);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ask_user
|
|
74
|
+
if (authz && authz.tools && authz.tools.ask_user) {
|
|
75
|
+
const s = authz.tools.ask_user;
|
|
76
|
+
if (s.mode !== 'off') {
|
|
77
|
+
feat.push('[ask_user](off|ask) → ' + s.mode);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// task tool
|
|
81
|
+
if (authz && authz.tools && authz.tools.task) {
|
|
82
|
+
const s = authz.tools.task;
|
|
83
|
+
if (s.mode !== 'off') {
|
|
84
|
+
feat.push('[task](off|ask|allow) → ' + s.mode);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// file tools
|
|
88
|
+
if (authz && authz.tools && authz.tools.file) {
|
|
89
|
+
const s = authz.tools.file;
|
|
90
|
+
if (s.mode !== 'off') {
|
|
91
|
+
feat.push('[file tools](off|ask|allow) → ' + s.mode + (s.mode === 'allowlist' ? ' (' + (Array.isArray(s.allowlist) ? s.allowlist.length : 0) + ' patterns)' : ''));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// app restart
|
|
96
|
+
if (authz && authz.tools && authz.tools.restart_app) {
|
|
97
|
+
const s = authz.tools.restart_app;
|
|
98
|
+
if (s.mode !== 'off') feat.push('[restart app](off|ask|allow) → ' + s.mode + ' — graceful supervisor relaunch from chat');
|
|
99
|
+
}
|
|
100
|
+
// --- MCP servers ---
|
|
101
|
+
if (Array.isArray(mcpServers) && mcpServers.length) {
|
|
102
|
+
|
|
103
|
+
// NOTE: the MCP line deliberately carries ONLY the configured server
|
|
104
|
+
// count — never the live running/stopped status. The feature summary
|
|
105
|
+
// rides inside the cached system block of Anthropic prompt caching;
|
|
106
|
+
// a status flip (on-demand start, error, restart) mid-conversation
|
|
107
|
+
// would change that block and invalidate the entire warm cache for
|
|
108
|
+
// the rest of the chat. The model can query live status via
|
|
109
|
+
// list_features / the MCP tools if it actually needs it.
|
|
110
|
+
feat.push('[MCP] ' + mcpServers.length + ' configured');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Agent files ---
|
|
114
|
+
// Resolve whether agent files are enabled (per-chat -> project -> profile default)
|
|
115
|
+
let agentFilesEnabled = false;
|
|
116
|
+
try {
|
|
117
|
+
const agentFiles = require('./agentFiles.js');
|
|
118
|
+
agentFilesEnabled = agentFiles.resolveEnabled({ chat, projectDir });
|
|
119
|
+
} catch { /* safe default */ }
|
|
120
|
+
if (agentFilesEnabled) {
|
|
121
|
+
feat.push('[agent files] enabled — AGENTS.md / CLAUDE.md / .github/copilot-instructions.md');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Project agents (subagent delegation targets) ---
|
|
125
|
+
try {
|
|
126
|
+
const agents = require('./agents.js');
|
|
127
|
+
const list = agents.list(projectDir);
|
|
128
|
+
if (list.length) {
|
|
129
|
+
feat.push('[agents] ' + list.length + ' available — subagent delegation targets');
|
|
130
|
+
}
|
|
131
|
+
} catch { /* safe default */ }
|
|
132
|
+
|
|
133
|
+
// --- File tagging ---
|
|
134
|
+
// Tagged files are always injected when the project has tag entries.
|
|
135
|
+
// We check if any tags are configured.
|
|
136
|
+
let tagsExist = false;
|
|
137
|
+
try {
|
|
138
|
+
const tags = require('./tags.js');
|
|
139
|
+
const tagList = tags.getTags(projectDir);
|
|
140
|
+
tagsExist = tagList && typeof tagList === 'object' && Object.keys(tagList).length > 0;
|
|
141
|
+
} catch { /* safe default */ }
|
|
142
|
+
if (tagsExist) {
|
|
143
|
+
feat.push('[file tagging] active — tags found in project');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Trace ---
|
|
147
|
+
const traceOn = chat && chat.trace === true;
|
|
148
|
+
if (traceOn) {
|
|
149
|
+
feat.push('[trace] on — events written to .mouaif/traces/');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Prompt profile ---
|
|
153
|
+
let profileId = 'average';
|
|
154
|
+
try {
|
|
155
|
+
const promptProfiles = require('./promptProfiles.js');
|
|
156
|
+
const profile = promptProfiles.resolveProfile({ chat, projectDir });
|
|
157
|
+
if (profile && profile.id) profileId = profile.id;
|
|
158
|
+
} catch { /* safe default */ }
|
|
159
|
+
feat.push('[prompt profile] ' + profileId);
|
|
160
|
+
|
|
161
|
+
if (!feat.length) return null;
|
|
162
|
+
|
|
163
|
+
lines.push('This mouaif project has these features:');
|
|
164
|
+
lines.push('');
|
|
165
|
+
for (const f of feat) lines.push('- ' + f);
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push('You also have a `list_features` tool available — call it to get the complete structured feature state with every detail.');
|
|
168
|
+
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// dispatchListFeatures(args, opts)
|
|
173
|
+
//
|
|
174
|
+
// Handler for the `list_features` tool call. Returns the full structured
|
|
175
|
+
// feature state as a JSON object so the model can inspect details.
|
|
176
|
+
async function dispatchListFeatures(args, opts) {
|
|
177
|
+
const chatId = opts && opts.chatId;
|
|
178
|
+
const projectDir = opts && opts.projectDir;
|
|
179
|
+
// Load the chat record to inspect trace, agentFiles, promptSize, etc.
|
|
180
|
+
let chat = opts && opts.chat;
|
|
181
|
+
if (!chat && projectDir && chatId) {
|
|
182
|
+
try {
|
|
183
|
+
const chats = require('./chats.js');
|
|
184
|
+
chat = chats.getChat(projectDir, chatId);
|
|
185
|
+
} catch { /* safe default */ }
|
|
186
|
+
}
|
|
187
|
+
const result = { ok: true, content: '', result: null };
|
|
188
|
+
|
|
189
|
+
const state = {};
|
|
190
|
+
|
|
191
|
+
// Built-in tool authorization
|
|
192
|
+
try {
|
|
193
|
+
const authz = require('./tools/authorization.js');
|
|
194
|
+
const authState = authz.getAuthorization(projectDir);
|
|
195
|
+
const tools = (authState && authState.tools) || {};
|
|
196
|
+
state.tools = {};
|
|
197
|
+
for (const name of ['shell', 'subagent', 'file', 'ask_user', 'report_progress', 'task', 'webpreview', 'restart_app']) {
|
|
198
|
+
const cfg = tools[name] || { mode: 'ask' };
|
|
199
|
+
state.tools[name] = {
|
|
200
|
+
mode: cfg.mode || 'ask',
|
|
201
|
+
allowlist: cfg.allowlist || [],
|
|
202
|
+
defaultTimeoutMs: cfg.defaultTimeoutMs,
|
|
203
|
+
maxTimeoutMs: cfg.maxTimeoutMs
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Surface the interpreter the shell tool runs under, so the model
|
|
207
|
+
// can pick the right syntax from the structured state too.
|
|
208
|
+
try {
|
|
209
|
+
const shellMod = require('./tools/shell.js');
|
|
210
|
+
const hint = typeof shellMod.shellDialectHint === 'function' ? shellMod.shellDialectHint() : '';
|
|
211
|
+
if (hint) {
|
|
212
|
+
state.tools.shell = Object.assign({}, state.tools.shell, {
|
|
213
|
+
dialect: hint,
|
|
214
|
+
exe: process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : (process.env.SHELL || '/bin/sh')
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch { /* shell module unavailable; keep auth state only */ }
|
|
218
|
+
} catch (e) {
|
|
219
|
+
state.tools = { _error: e.message };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// MCP servers
|
|
223
|
+
try {
|
|
224
|
+
const mcpMod = require('./mcp.js');
|
|
225
|
+
const serverList = typeof mcpMod.listServers === 'function' ? mcpMod.listServers(projectDir) : [];
|
|
226
|
+
if (Array.isArray(serverList)) {
|
|
227
|
+
state.mcp = [];
|
|
228
|
+
for (const s of serverList) {
|
|
229
|
+
const entry = { name: s.name, slug: s.slug, status: s.status, tools: s.tools || [] };
|
|
230
|
+
if (s.authorization) entry.authorization = s.authorization;
|
|
231
|
+
state.mcp.push(entry);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
state.mcp = { _error: e.message };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Agent files
|
|
239
|
+
try {
|
|
240
|
+
const agentFiles = require('./agentFiles.js');
|
|
241
|
+
state.agentFiles = {
|
|
242
|
+
enabled: agentFiles.resolveEnabled({ chat, projectDir }),
|
|
243
|
+
fileNames: agentFiles.resolveFileNames({ chat, projectDir }),
|
|
244
|
+
discovered: agentFiles.discover(projectDir).map(f => ({ name: f.name, size: f.size }))
|
|
245
|
+
};
|
|
246
|
+
} catch (e) {
|
|
247
|
+
state.agentFiles = { _error: e.message };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Project agents (subagent delegation targets)
|
|
251
|
+
try {
|
|
252
|
+
const agents = require('./agents.js');
|
|
253
|
+
state.agents = {
|
|
254
|
+
discovered: agents.list(projectDir).map(a => ({ name: a.name }))
|
|
255
|
+
};
|
|
256
|
+
} catch (e) {
|
|
257
|
+
state.agents = { _error: e.message };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// File tagging
|
|
261
|
+
try {
|
|
262
|
+
const tags = require('./tags.js');
|
|
263
|
+
const tagList = tags.getTags(projectDir);
|
|
264
|
+
state.fileTagging = {
|
|
265
|
+
active: tagList && typeof tagList === 'object' && Object.keys(tagList).length > 0,
|
|
266
|
+
count: tagList && typeof tagList === 'object' ? Object.keys(tagList).length : 0
|
|
267
|
+
};
|
|
268
|
+
} catch (e) {
|
|
269
|
+
state.fileTagging = { _error: e.message };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Trace
|
|
273
|
+
state.trace = chat && chat.trace === true;
|
|
274
|
+
|
|
275
|
+
// Prompt profile
|
|
276
|
+
try {
|
|
277
|
+
const promptProfiles = require('./promptProfiles.js');
|
|
278
|
+
const profile = promptProfiles.resolveProfile({ chat, projectDir });
|
|
279
|
+
state.promptProfile = profile ? { id: profile.id, label: profile.label } : null;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
state.promptProfile = null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
result.content = JSON.stringify(state, null, 2);
|
|
285
|
+
result.result = state;
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
TOOL_NAME,
|
|
291
|
+
LIST_FEATURES_SPEC,
|
|
292
|
+
buildFeatureSummary,
|
|
293
|
+
dispatchListFeatures
|
|
294
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Agent files — well-known instruction files at the project root that
|
|
4
|
+
// coding assistants read (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md).
|
|
5
|
+
//
|
|
6
|
+
// When enabled for a chat, the content of every agent file found at the
|
|
7
|
+
// project root is injected into the upstream system context, after the
|
|
8
|
+
// prompt-size profile and before the custom prompt. Each file rides as
|
|
9
|
+
// its own `system` message so the model can see the file boundary.
|
|
10
|
+
//
|
|
11
|
+
// Discovery is read-only: we never create, modify, or delete these files.
|
|
12
|
+
// Missing files are silently skipped. Files larger than MAX_BYTES are
|
|
13
|
+
// truncated so a huge AGENTS.md does not blow the context budget.
|
|
14
|
+
//
|
|
15
|
+
// Public surface:
|
|
16
|
+
// DEFAULT_AGENT_FILE_NAMES : ordered list of basenames we look for
|
|
17
|
+
// MAX_BYTES : per-file size cap (default 64 KiB)
|
|
18
|
+
// discover(projectDir) -> [{ name, absPath, size }]
|
|
19
|
+
// load(projectDir) -> [{ name, role, content }]
|
|
20
|
+
// isEnabledForChat(chat) -> boolean
|
|
21
|
+
// resolveEnabled({chat, projectDir}) -> boolean
|
|
22
|
+
// resolveFileNames({chat, projectDir}) -> string[]
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const settings = require('./settings.js');
|
|
27
|
+
|
|
28
|
+
// Basenames we look for, in priority order. First match wins per slot;
|
|
29
|
+
// all matches are injected (they are complementary, not exclusive).
|
|
30
|
+
// The project-level `agentFileNames` setting can override this list.
|
|
31
|
+
const DEFAULT_AGENT_FILE_NAMES = [
|
|
32
|
+
'AGENTS.md',
|
|
33
|
+
'CLAUDE.md',
|
|
34
|
+
'.github/copilot-instructions.md'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Per-file size cap. Files larger than this are truncated to the cap
|
|
38
|
+
// with a trailing note so the model knows it is not the whole file.
|
|
39
|
+
const MAX_BYTES = 64 * 1024;
|
|
40
|
+
|
|
41
|
+
// resolveFileNames({ chat, projectDir }) -> string[]
|
|
42
|
+
//
|
|
43
|
+
// Return the list of basenames to look for. Resolution order:
|
|
44
|
+
// 1. project.agentFileNames (array of strings) — explicit override
|
|
45
|
+
// 2. DEFAULT_AGENT_FILE_NAMES
|
|
46
|
+
// The chat record does not carry its own file list; the per-chat toggle
|
|
47
|
+
// is a boolean on/off, not a per-file picker.
|
|
48
|
+
function resolveFileNames({ chat, projectDir }) {
|
|
49
|
+
void chat; // reserved for future per-chat file lists
|
|
50
|
+
if (projectDir) {
|
|
51
|
+
try {
|
|
52
|
+
const project = settings.getProject(projectDir);
|
|
53
|
+
if (project && Array.isArray(project.agentFileNames) && project.agentFileNames.length) {
|
|
54
|
+
return project.agentFileNames.map(String).filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
} catch { /* fall through to default */ }
|
|
57
|
+
}
|
|
58
|
+
return DEFAULT_AGENT_FILE_NAMES.slice();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// discover(projectDir, names?) -> [{ name, absPath, size }]
|
|
62
|
+
//
|
|
63
|
+
// Return the agent files that exist at the project root. Missing files
|
|
64
|
+
// are skipped silently. The result is ordered by the names list so the
|
|
65
|
+
// most specific file (AGENTS.md) is first. If no names are given the
|
|
66
|
+
// project-level/default list is used.
|
|
67
|
+
function discover(projectDir, names) {
|
|
68
|
+
if (!projectDir || typeof projectDir !== 'string') return [];
|
|
69
|
+
const list = Array.isArray(names) && names.length ? names : resolveFileNames({ projectDir });
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const name of list) {
|
|
72
|
+
const absPath = path.join(projectDir, name);
|
|
73
|
+
try {
|
|
74
|
+
const st = fs.statSync(absPath);
|
|
75
|
+
if (st.isFile()) out.push({ name, absPath, size: st.size });
|
|
76
|
+
} catch { /* missing or unreadable — skip */ }
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// load(projectDir, names?) -> [{ name, role, content }]
|
|
82
|
+
//
|
|
83
|
+
// Read every discovered agent file and return it as a system-message
|
|
84
|
+
// payload. Files larger than MAX_BYTES are truncated. Unreadable files
|
|
85
|
+
// are skipped. The content is wrapped in a small header so the model
|
|
86
|
+
// knows which file it came from. If no names are given the
|
|
87
|
+
// project-level/default list is used.
|
|
88
|
+
function load(projectDir, names) {
|
|
89
|
+
const files = discover(projectDir, names);
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const f of files) {
|
|
92
|
+
let text;
|
|
93
|
+
try {
|
|
94
|
+
const buf = fs.readFileSync(f.absPath);
|
|
95
|
+
if (buf.length > MAX_BYTES) {
|
|
96
|
+
text = buf.slice(0, MAX_BYTES).toString('utf8')
|
|
97
|
+
+ '\n\n[... truncated at ' + MAX_BYTES + ' bytes ...]';
|
|
98
|
+
} else {
|
|
99
|
+
text = buf.toString('utf8');
|
|
100
|
+
}
|
|
101
|
+
} catch { continue; }
|
|
102
|
+
if (!text.trim()) continue;
|
|
103
|
+
out.push({
|
|
104
|
+
name: f.name,
|
|
105
|
+
role: 'system',
|
|
106
|
+
content: 'Project agent instructions from ' + f.name + ':\n\n' + text
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// isEnabledForChat(chat) -> boolean
|
|
113
|
+
//
|
|
114
|
+
// Per-chat toggle. The chat record carries an `agentFiles` field:
|
|
115
|
+
// true -> inject agent files
|
|
116
|
+
// false -> do not inject
|
|
117
|
+
// undefined / null -> fall through to the profile default
|
|
118
|
+
// The profile default is: very-small = OFF, average/extensive = ON.
|
|
119
|
+
function isEnabledForChat(chat) {
|
|
120
|
+
if (!chat || typeof chat !== 'object') return true;
|
|
121
|
+
if (chat.agentFiles === false) return false;
|
|
122
|
+
if (chat.agentFiles === true) return true;
|
|
123
|
+
// No explicit per-chat choice: fall back to the prompt-size profile.
|
|
124
|
+
// very-small is meant to be the tightest context, so agent files are
|
|
125
|
+
// off there. average and extensive get them by default.
|
|
126
|
+
const size = chat.promptSize || 'average';
|
|
127
|
+
return size !== 'very-small';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// resolveEnabled({ chat, projectDir }) -> boolean
|
|
131
|
+
//
|
|
132
|
+
// Full resolution: project-level gate -> per-chat override -> profile default.
|
|
133
|
+
// The project-level setting is the master switch: if it is explicitly `false`
|
|
134
|
+
// agent files are disabled regardless of the per-chat toggle. The per-chat
|
|
135
|
+
// toggle can only *disable* (set to false) when the project has it on.
|
|
136
|
+
// If neither is explicitly set the profile default applies.
|
|
137
|
+
function resolveEnabled({ chat, projectDir }) {
|
|
138
|
+
// 1. Project-level master switch
|
|
139
|
+
if (projectDir) {
|
|
140
|
+
try {
|
|
141
|
+
const project = settings.getProject(projectDir);
|
|
142
|
+
if (project && typeof project.agentFiles === 'boolean') {
|
|
143
|
+
if (project.agentFiles === false) return false; // locked off
|
|
144
|
+
// project is true — per-chat can still override to false
|
|
145
|
+
if (chat && chat.agentFiles === false) return false;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* fall through */ }
|
|
149
|
+
}
|
|
150
|
+
// 2. Per-chat explicit (no project-level setting)
|
|
151
|
+
if (chat && typeof chat.agentFiles === 'boolean') return chat.agentFiles;
|
|
152
|
+
// 3. Profile default
|
|
153
|
+
return isEnabledForChat(chat);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
DEFAULT_AGENT_FILE_NAMES,
|
|
158
|
+
MAX_BYTES,
|
|
159
|
+
discover,
|
|
160
|
+
load,
|
|
161
|
+
isEnabledForChat,
|
|
162
|
+
resolveEnabled,
|
|
163
|
+
resolveFileNames
|
|
164
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Agent Skills-compatible discovery and progressive activation.
|
|
4
|
+
// Format: https://agentskills.io/specification
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const settings = require('./settings.js');
|
|
9
|
+
const MAX_BYTES = 512 * 1024;
|
|
10
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
|
+
|
|
12
|
+
function parseScalar(raw) {
|
|
13
|
+
const s = String(raw || '').trim();
|
|
14
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1);
|
|
15
|
+
return s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Parse the spec's shallow frontmatter fields without adding a YAML runtime.
|
|
19
|
+
// metadata is retained as string pairs; malformed/unsupported YAML is diagnosed.
|
|
20
|
+
function parseSkillFile(text, directoryName) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
if (!text.startsWith('---\n') && !text.startsWith('---\r\n')) return { valid: false, diagnostics: ['missing YAML frontmatter'] };
|
|
23
|
+
const lines = text.split(/\r?\n/);
|
|
24
|
+
const close = lines.indexOf('---', 1);
|
|
25
|
+
if (close < 0) return { valid: false, diagnostics: ['unterminated YAML frontmatter'] };
|
|
26
|
+
const metadata = {};
|
|
27
|
+
let map = null;
|
|
28
|
+
for (const line of lines.slice(1, close)) {
|
|
29
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
30
|
+
const nested = line.match(/^\s+([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
31
|
+
if (nested && map) { map[nested[1]] = parseScalar(nested[2]); continue; }
|
|
32
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
33
|
+
if (!m) { diagnostics.push('unsupported YAML line: ' + line.trim()); continue; }
|
|
34
|
+
if (m[1] === 'metadata' && !m[2]) { metadata.metadata = {}; map = metadata.metadata; continue; }
|
|
35
|
+
map = null;
|
|
36
|
+
metadata[m[1]] = parseScalar(m[2]);
|
|
37
|
+
}
|
|
38
|
+
const name = metadata.name || '';
|
|
39
|
+
const description = metadata.description || '';
|
|
40
|
+
if (!name) diagnostics.push('name is required');
|
|
41
|
+
if (!description) diagnostics.push('description is required');
|
|
42
|
+
if (name && (!NAME_RE.test(name) || name.length > 64 || name !== directoryName)) diagnostics.push('name must match its directory and use 1-64 lowercase letters, numbers, or single hyphens');
|
|
43
|
+
if (description.length > 1024) diagnostics.push('description exceeds 1024 characters');
|
|
44
|
+
if (metadata.compatibility && metadata.compatibility.length > 500) diagnostics.push('compatibility exceeds 500 characters');
|
|
45
|
+
const validName = !!name && NAME_RE.test(name) && name.length <= 64 && name === directoryName;
|
|
46
|
+
const validDescription = !!description && description.length <= 1024;
|
|
47
|
+
const validCompatibility = !metadata.compatibility || metadata.compatibility.length <= 500;
|
|
48
|
+
return { valid: validName && validDescription && validCompatibility, name, description, metadata, body: lines.slice(close + 1).join('\n').trim(), diagnostics };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeSkillFile(projectDir, root, entry) {
|
|
52
|
+
const file = path.join(root, entry.name, 'SKILL.md');
|
|
53
|
+
try {
|
|
54
|
+
const realRoot = fs.realpathSync(projectDir);
|
|
55
|
+
const realFile = fs.realpathSync(file);
|
|
56
|
+
if (realFile !== realRoot && !realFile.startsWith(realRoot + path.sep)) return null;
|
|
57
|
+
const stat = fs.statSync(realFile);
|
|
58
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) return null;
|
|
59
|
+
const text = fs.readFileSync(realFile, 'utf8');
|
|
60
|
+
const parsed = parseSkillFile(text, entry.name);
|
|
61
|
+
if (!parsed.valid) return null;
|
|
62
|
+
return Object.assign({ id: parsed.name, path: path.relative(projectDir, realFile).replace(/\\/g, '/'), absPath: realFile, root: path.dirname(realFile), size: stat.size }, parsed);
|
|
63
|
+
} catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function discover(projectDir) {
|
|
67
|
+
if (!projectDir || typeof projectDir !== 'string') return [];
|
|
68
|
+
const root = path.join(projectDir, '.agents', 'skills');
|
|
69
|
+
let entries;
|
|
70
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return []; }
|
|
71
|
+
return entries.filter((e) => e.isDirectory()).map((e) => safeSkillFile(projectDir, root, e)).filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// normalizeDisabledSkills(raw) -> Set<string>
|
|
75
|
+
//
|
|
76
|
+
// Both the project and the chat carry a `disabledSkills` list of skill
|
|
77
|
+
// ids/names. Anything that is not a string list is treated as empty so a
|
|
78
|
+
// hand-edited `.mouaif.json` (or a legacy row) can never take discovery down.
|
|
79
|
+
function normalizeDisabledSkills(raw) {
|
|
80
|
+
if (!Array.isArray(raw)) return new Set();
|
|
81
|
+
return new Set(raw.map((n) => String(n)).filter(Boolean));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// resolve({ chat, projectDir }) -> {
|
|
85
|
+
// enabled, projectEnabled, disabled, projectDisabled, chatDisabled, skills
|
|
86
|
+
// }
|
|
87
|
+
//
|
|
88
|
+
// `disabled` is the union used for filtering (catalog + activate_skill).
|
|
89
|
+
// The two halves are kept separate so the chat UI can tell a skill the
|
|
90
|
+
// project switched off (locked, with a reason) from one the user
|
|
91
|
+
// unchecked in this chat (a plain per-chat opt-out).
|
|
92
|
+
function resolve({ chat, projectDir }) {
|
|
93
|
+
let project = {};
|
|
94
|
+
try { project = settings.getProject(projectDir) || {}; } catch { /* defaults */ }
|
|
95
|
+
const projectEnabled = project.skills !== false;
|
|
96
|
+
const enabled = projectEnabled && !(chat && chat.skills === false);
|
|
97
|
+
const projectDisabled = normalizeDisabledSkills(project.disabledSkills);
|
|
98
|
+
const chatDisabled = normalizeDisabledSkills(chat && chat.disabledSkills);
|
|
99
|
+
const disabled = new Set([...projectDisabled, ...chatDisabled]);
|
|
100
|
+
return { enabled, projectEnabled, disabled, projectDisabled, chatDisabled, skills: discover(projectDir) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function available(projectDir, chat) {
|
|
104
|
+
const state = resolve({ chat, projectDir });
|
|
105
|
+
return state.enabled ? state.skills.filter((s) => !state.disabled.has(s.name)) : [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function catalogMessage(projectDir, chat) {
|
|
109
|
+
const skills = available(projectDir, chat);
|
|
110
|
+
if (!skills.length) return '';
|
|
111
|
+
return 'Available Agent Skills (metadata only):\n' + skills.map((s) => '- ' + s.name + ': ' + s.description).join('\n')
|
|
112
|
+
+ '\nWhen a task matches a skill, call activate_skill with its name before proceeding. Load referenced resources only as needed, relative to the returned skill directory.';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSpec(projectDir, chat) {
|
|
116
|
+
const names = available(projectDir, chat).map((s) => s.name);
|
|
117
|
+
if (!names.length) return null;
|
|
118
|
+
return { type: 'function', function: { name: 'activate_skill', description: 'Load one available Agent Skill when its description matches the task.', parameters: { type: 'object', properties: { name: { type: 'string', enum: names, description: 'Skill name from the available skills catalog.' } }, required: ['name'], additionalProperties: false } } };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function listResources(skill) {
|
|
122
|
+
const out = [];
|
|
123
|
+
function walk(dir, depth) {
|
|
124
|
+
if (depth > 3 || out.length >= 200) return;
|
|
125
|
+
let entries;
|
|
126
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (out.length >= 200) break;
|
|
129
|
+
const abs = path.join(dir, entry.name);
|
|
130
|
+
if (entry.isDirectory()) walk(abs, depth + 1);
|
|
131
|
+
else if (entry.isFile() && abs !== skill.absPath) out.push(path.relative(skill.root, abs).replace(/\\/g, '/'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
walk(skill.root, 0);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function activate(projectDir, chat, name) {
|
|
139
|
+
const skill = available(projectDir, chat).find((s) => s.name === name);
|
|
140
|
+
if (!skill) { const e = new Error('skill is unavailable: ' + name); e.code = 'ENO_SKILL'; throw e; }
|
|
141
|
+
const resources = listResources(skill);
|
|
142
|
+
const content = '<skill_content name="' + skill.name + '">\n' + skill.body + '\n\nSkill directory: ' + skill.root
|
|
143
|
+
+ '\nRelative paths are relative to this directory.' + (resources.length ? '\nResources (load only when needed):\n' + resources.map((r) => '- ' + r).join('\n') : '') + '\n</skill_content>';
|
|
144
|
+
return { ok: true, content, result: { name: skill.name, description: skill.description, directory: skill.root, resources, content } };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { MAX_BYTES, NAME_RE, parseSkillFile, discover, resolve, available, catalogMessage, buildSpec, activate };
|