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/chats.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Chats — per-project chat list.
|
|
3
|
+
//
|
|
4
|
+
// Chat metadata and messages are persisted in the app-level SQLite store
|
|
5
|
+
// (~/.mouaif/store.sqlite) via src/chatdb.js. There is no file-based chat
|
|
6
|
+
// storage backend anymore: `.mouaif.messages.*.json` transcripts were
|
|
7
|
+
// removed, and the opt-in trace-to-file export (docs/decisions.md §5) is the
|
|
8
|
+
// only way to commit a chat's transcript next to the project source.
|
|
9
|
+
//
|
|
10
|
+
// Each project (identified by its on-disk `projectDir`) owns a list of
|
|
11
|
+
// chats. The public API shape delegates to src/chatdb.js.
|
|
12
|
+
//
|
|
13
|
+
// Schema (per chat):
|
|
14
|
+
// { id, title, createdAt, lastOpenedAt, trace, promptSize, promptId,
|
|
15
|
+
// providerId, modelId, draft, tools, agentFiles, skills, toolAuth }
|
|
16
|
+
//
|
|
17
|
+
// API enrichment (added by GET /api/chats, NOT persisted):
|
|
18
|
+
// { totalCost: { total, known, currency } }
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const settings = require('./settings.js');
|
|
22
|
+
const messages = require('./messages.js');
|
|
23
|
+
const PROJECT_FILE = '.mouaif.json';
|
|
24
|
+
function ensureDir(dir) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
function newChatId() {
|
|
28
|
+
return crypto.randomBytes(4).toString('hex'); // 8 hex chars
|
|
29
|
+
}
|
|
30
|
+
function titleFromPrompt(content) {
|
|
31
|
+
const text = String(content || '').replace(/\s+/g, ' ').trim();
|
|
32
|
+
if (!text) return 'New chat';
|
|
33
|
+
const max = 60;
|
|
34
|
+
if (text.length <= max) return text;
|
|
35
|
+
return text.slice(0, max).replace(/[\s.,;:!?\-–—]+$/g, '') + '…';
|
|
36
|
+
}
|
|
37
|
+
function isDefaultTitle(title) {
|
|
38
|
+
const t = String(title || '').trim();
|
|
39
|
+
return !t || t === 'New chat';
|
|
40
|
+
}
|
|
41
|
+
function defaultsForProject(project) {
|
|
42
|
+
return {
|
|
43
|
+
promptSize: (project && project.promptSize) || 'average'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function getChatDb() {
|
|
47
|
+
return require('./chatdb.js');
|
|
48
|
+
}
|
|
49
|
+
// ---- Public surface -------------------------------------------------------
|
|
50
|
+
// Per-chat authorization overrides live directly on the chat record
|
|
51
|
+
// (`chat.toolAuth`), so reading them is just a getChat() — see
|
|
52
|
+
// `readChatAuthOverrides` in src/tools/authorization.js for the shape.
|
|
53
|
+
|
|
54
|
+
function listChats(projectDir, options) {
|
|
55
|
+
return getChatDb().listChats(projectDir, options);
|
|
56
|
+
}
|
|
57
|
+
function countChats(projectDir) {
|
|
58
|
+
return getChatDb().countChats(projectDir);
|
|
59
|
+
}
|
|
60
|
+
function getChat(projectDir, chatId) {
|
|
61
|
+
if (!chatId || typeof chatId !== 'string') return null;
|
|
62
|
+
return getChatDb().getChat(projectDir, chatId);
|
|
63
|
+
}
|
|
64
|
+
function createChat(projectDir, opts) {
|
|
65
|
+
ensureDir(projectDir);
|
|
66
|
+
const resolved = settings.getResolved(projectDir);
|
|
67
|
+
const defaults = defaultsForProject(resolved);
|
|
68
|
+
const chat = {
|
|
69
|
+
id: newChatId(),
|
|
70
|
+
title: (opts && typeof opts.title === 'string' && opts.title.trim()) ? opts.title.trim() : 'New chat',
|
|
71
|
+
createdAt: new Date().toISOString(),
|
|
72
|
+
lastOpenedAt: null,
|
|
73
|
+
trace: opts && opts.trace === true,
|
|
74
|
+
promptSize: opts && ['very-small', 'average', 'extensive'].includes(opts.promptSize) ? opts.promptSize : defaults.promptSize,
|
|
75
|
+
thinkingLevel: opts && typeof opts.thinkingLevel === 'string' ? opts.thinkingLevel : '',
|
|
76
|
+
maxOutputTokens: opts && typeof opts.maxOutputTokens === 'string' ? opts.maxOutputTokens : '',
|
|
77
|
+
promptId: opts && typeof opts.promptId === 'string' && opts.promptId ? opts.promptId : null,
|
|
78
|
+
skills: opts && typeof opts.skills === 'boolean' ? opts.skills : undefined,
|
|
79
|
+
disabledSkills: opts && Array.isArray(opts.disabledSkills) && opts.disabledSkills.length
|
|
80
|
+
? opts.disabledSkills.map((n) => String(n)).filter(Boolean)
|
|
81
|
+
: undefined,
|
|
82
|
+
autoRetry: opts && typeof opts.autoRetry === 'boolean' ? opts.autoRetry : undefined,
|
|
83
|
+
// Per-chat tool authorization overrides — see updateChat below.
|
|
84
|
+
toolAuth: (opts && opts.toolAuth && typeof opts.toolAuth === 'object' && !Array.isArray(opts.toolAuth) && Object.keys(opts.toolAuth).length)
|
|
85
|
+
? opts.toolAuth
|
|
86
|
+
: undefined,
|
|
87
|
+
tools: opts && Array.isArray(opts.tools) ? opts.tools : undefined
|
|
88
|
+
};
|
|
89
|
+
return getChatDb().createChat(projectDir, chat);
|
|
90
|
+
}
|
|
91
|
+
function updateChat(projectDir, chatId, patch) {
|
|
92
|
+
if (!chatId) return null;
|
|
93
|
+
const dbPatch = {};
|
|
94
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'title')) {
|
|
95
|
+
const t = String(patch.title).trim();
|
|
96
|
+
if (t) dbPatch.title = t;
|
|
97
|
+
}
|
|
98
|
+
if (patch && typeof patch.trace === 'boolean') dbPatch.trace = patch.trace;
|
|
99
|
+
if (patch && ['very-small', 'average', 'extensive'].includes(patch.promptSize)) dbPatch.promptSize = patch.promptSize;
|
|
100
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'promptId')) {
|
|
101
|
+
dbPatch.promptId = (patch.promptId === null || patch.promptId === '') ? null : String(patch.promptId);
|
|
102
|
+
}
|
|
103
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'providerId')) {
|
|
104
|
+
dbPatch.providerId = (patch.providerId === null || patch.providerId === '') ? null : String(patch.providerId);
|
|
105
|
+
}
|
|
106
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'modelId')) {
|
|
107
|
+
dbPatch.modelId = (patch.modelId === null || patch.modelId === '') ? null : String(patch.modelId);
|
|
108
|
+
}
|
|
109
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'thinkingLevel')) {
|
|
110
|
+
dbPatch.thinkingLevel = typeof patch.thinkingLevel === 'string' ? patch.thinkingLevel : '';
|
|
111
|
+
}
|
|
112
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'maxOutputTokens')) {
|
|
113
|
+
dbPatch.maxOutputTokens = typeof patch.maxOutputTokens === 'string' ? patch.maxOutputTokens : '';
|
|
114
|
+
}
|
|
115
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'draft')) {
|
|
116
|
+
dbPatch.draft = typeof patch.draft === 'string' ? patch.draft : '';
|
|
117
|
+
}
|
|
118
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'draftAttachments')) {
|
|
119
|
+
const attachments = messages.normalizeAttachments(patch.draftAttachments);
|
|
120
|
+
dbPatch.draftAttachments = attachments.length ? attachments : null;
|
|
121
|
+
}
|
|
122
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'tools')) {
|
|
123
|
+
if (patch.tools === null) {
|
|
124
|
+
dbPatch.tools = null;
|
|
125
|
+
} else if (Array.isArray(patch.tools)) {
|
|
126
|
+
dbPatch.tools = patch.tools.map((n) => String(n)).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'agentFiles')) {
|
|
130
|
+
dbPatch.agentFiles = (patch.agentFiles === null || patch.agentFiles === undefined)
|
|
131
|
+
? undefined
|
|
132
|
+
: patch.agentFiles === true;
|
|
133
|
+
}
|
|
134
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'skills')) {
|
|
135
|
+
dbPatch.skills = (patch.skills === null || patch.skills === undefined)
|
|
136
|
+
? undefined
|
|
137
|
+
: patch.skills === true;
|
|
138
|
+
}
|
|
139
|
+
// Per-skill opt-outs for this one chat. `null` (or an empty list)
|
|
140
|
+
// clears them; anything that is not a list is ignored so a malformed
|
|
141
|
+
// PATCH cannot wipe the field.
|
|
142
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'disabledSkills')) {
|
|
143
|
+
const list = Array.isArray(patch.disabledSkills)
|
|
144
|
+
? Array.from(new Set(patch.disabledSkills.map((n) => String(n)).filter(Boolean)))
|
|
145
|
+
: [];
|
|
146
|
+
dbPatch.disabledSkills = list.length ? list : null;
|
|
147
|
+
}
|
|
148
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'autoRetry')) {
|
|
149
|
+
dbPatch.autoRetry = patch.autoRetry === true;
|
|
150
|
+
}
|
|
151
|
+
// Per-chat tool authorization overrides (decisions §17). The shape is the
|
|
152
|
+
// structured one the authorization module owns:
|
|
153
|
+
// { native: { shell: { mode, allowlist }, … },
|
|
154
|
+
// mcp: { shared: …, servers: { <slug>: { mode } }, tools: { <name>: { mode } } } }
|
|
155
|
+
// Only `mode` (and a native `allowlist`) are persisted; everything else is
|
|
156
|
+
// dropped so the chat record can never accumulate arbitrary payloads. An
|
|
157
|
+
// empty map is stored as `null`: "no overrides" has exactly one
|
|
158
|
+
// representation. Anything that is not an object is ignored so a malformed
|
|
159
|
+
// PATCH cannot wipe the field.
|
|
160
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'toolAuth')) {
|
|
161
|
+
if (patch.toolAuth === null) {
|
|
162
|
+
dbPatch.toolAuth = undefined;
|
|
163
|
+
} else if (patch.toolAuth && typeof patch.toolAuth === 'object' && !Array.isArray(patch.toolAuth)) {
|
|
164
|
+
const next = {};
|
|
165
|
+
const native = (patch.toolAuth.native && typeof patch.toolAuth.native === 'object' && !Array.isArray(patch.toolAuth.native))
|
|
166
|
+
? patch.toolAuth.native
|
|
167
|
+
: {};
|
|
168
|
+
const nativeOut = {};
|
|
169
|
+
for (const [name, value] of Object.entries(native)) {
|
|
170
|
+
if (typeof name !== 'string' || !name) continue;
|
|
171
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
172
|
+
if (typeof value.mode !== 'string' || !value.mode) continue;
|
|
173
|
+
nativeOut[name] = {
|
|
174
|
+
mode: value.mode,
|
|
175
|
+
allowlist: Array.isArray(value.allowlist) ? value.allowlist.map((p) => String(p)).filter(Boolean) : []
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(nativeOut).length) next.native = nativeOut;
|
|
179
|
+
const m = patch.toolAuth.mcp;
|
|
180
|
+
if (m && typeof m === 'object' && !Array.isArray(m)) {
|
|
181
|
+
const mcpOut = {};
|
|
182
|
+
if (m.shared && typeof m.shared === 'object' && typeof m.shared.mode === 'string' && m.shared.mode) {
|
|
183
|
+
mcpOut.shared = { mode: m.shared.mode };
|
|
184
|
+
}
|
|
185
|
+
for (const key of ['servers', 'tools']) {
|
|
186
|
+
const map = (m[key] && typeof m[key] === 'object' && !Array.isArray(m[key])) ? m[key] : null;
|
|
187
|
+
if (!map) continue;
|
|
188
|
+
const kept = {};
|
|
189
|
+
for (const [name, value] of Object.entries(map)) {
|
|
190
|
+
if (typeof name !== 'string' || !name) continue;
|
|
191
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
192
|
+
if (typeof value.mode !== 'string' || !value.mode) continue;
|
|
193
|
+
kept[name] = { mode: value.mode };
|
|
194
|
+
}
|
|
195
|
+
if (Object.keys(kept).length) mcpOut[key] = kept;
|
|
196
|
+
}
|
|
197
|
+
if (Object.keys(mcpOut).length) next.mcp = mcpOut;
|
|
198
|
+
}
|
|
199
|
+
dbPatch.toolAuth = Object.keys(next).length ? next : undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'lastOpenedAt')) {
|
|
203
|
+
dbPatch.lastOpenedAt = patch.lastOpenedAt;
|
|
204
|
+
}
|
|
205
|
+
return getChatDb().updateChat(projectDir, chatId, dbPatch);
|
|
206
|
+
}
|
|
207
|
+
function deleteChat(projectDir, chatId) {
|
|
208
|
+
if (!chatId) return false;
|
|
209
|
+
return getChatDb().deleteChat(projectDir, chatId);
|
|
210
|
+
}
|
|
211
|
+
function touchChat(projectDir, chatId) {
|
|
212
|
+
return updateChat(projectDir, chatId, { lastOpenedAt: new Date().toISOString() });
|
|
213
|
+
}
|
|
214
|
+
function titleChatFromPrompt(projectDir, chatId, content) {
|
|
215
|
+
if (!chatId) return null;
|
|
216
|
+
const title = titleFromPrompt(content);
|
|
217
|
+
if (isDefaultTitle(title)) return null;
|
|
218
|
+
const current = getChat(projectDir, chatId);
|
|
219
|
+
if (!current || !isDefaultTitle(current.title)) return current;
|
|
220
|
+
return updateChat(projectDir, chatId, { title });
|
|
221
|
+
}
|
|
222
|
+
function clearPromptId(projectDir, promptId) {
|
|
223
|
+
if (!projectDir || !promptId) return 0;
|
|
224
|
+
const chats = listChats(projectDir);
|
|
225
|
+
let changed = 0;
|
|
226
|
+
for (const c of chats) {
|
|
227
|
+
if (c.promptId === promptId) {
|
|
228
|
+
updateChat(projectDir, c.id, { promptId: null });
|
|
229
|
+
changed++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return changed;
|
|
233
|
+
}
|
|
234
|
+
function chatTotalCost(projectDir, chatId) {
|
|
235
|
+
const chat = getChat(projectDir, chatId);
|
|
236
|
+
return chat && chat.totalCost
|
|
237
|
+
? { total: chat.totalCost.total, known: chat.totalCost.known, currency: 'USD' }
|
|
238
|
+
: { total: 0, known: false, currency: 'USD' };
|
|
239
|
+
}
|
|
240
|
+
function recomputeProjectTotalCost(projectDir) {
|
|
241
|
+
const db = getChatDb();
|
|
242
|
+
const totals = db.projectCostTotals(projectDir);
|
|
243
|
+
let total = 0;
|
|
244
|
+
let knownCount = 0;
|
|
245
|
+
for (const chat of listChats(projectDir)) {
|
|
246
|
+
const cost = totals[chat.id] || { total: 0, known: false, knownCount: 0 };
|
|
247
|
+
db.updateChat(projectDir, chat.id, {
|
|
248
|
+
totalCost: {
|
|
249
|
+
total: cost.total,
|
|
250
|
+
known: cost.known,
|
|
251
|
+
currency: 'USD',
|
|
252
|
+
knownCount: cost.knownCount || 0
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
total += cost.total;
|
|
256
|
+
knownCount += cost.knownCount || 0;
|
|
257
|
+
}
|
|
258
|
+
const out = { total, known: knownCount > 0, currency: 'USD', knownCount };
|
|
259
|
+
try { require('./projects.js').setProjectTotalCost(projectDir, out); } catch { /* non-fatal */ }
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
module.exports = {
|
|
263
|
+
PROJECT_FILE,
|
|
264
|
+
listChats,
|
|
265
|
+
countChats,
|
|
266
|
+
getChat,
|
|
267
|
+
createChat,
|
|
268
|
+
updateChat,
|
|
269
|
+
deleteChat,
|
|
270
|
+
touchChat,
|
|
271
|
+
titleChatFromPrompt,
|
|
272
|
+
clearPromptId,
|
|
273
|
+
chatTotalCost,
|
|
274
|
+
recomputeProjectTotalCost
|
|
275
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Project custom actions: user-authored one-tap shortcuts backed by either
|
|
3
|
+
// the native shell runner or a configured MCP tool. The definitions live in
|
|
4
|
+
// the project settings under `customActions`; execution still goes through
|
|
5
|
+
// the underlying shell/MCP authorization gate.
|
|
6
|
+
const settings = require('./settings.js');
|
|
7
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
8
|
+
const KINDS = new Set(['cli', 'mcp']);
|
|
9
|
+
function typedError(code, message) {
|
|
10
|
+
return Object.assign(new Error(message), { code });
|
|
11
|
+
}
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function cleanText(value, max) {
|
|
16
|
+
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
17
|
+
}
|
|
18
|
+
function normalizeAction(raw, index) {
|
|
19
|
+
if (!isPlainObject(raw)) throw typedError('EBADINPUT', 'custom action ' + (index + 1) + ' must be an object');
|
|
20
|
+
const id = cleanText(raw.id, 64);
|
|
21
|
+
if (!ID_RE.test(id)) throw typedError('EBADINPUT', 'custom action id must start with a letter or digit and use only letters, digits, ., _, or -');
|
|
22
|
+
const kind = cleanText(raw.kind, 16);
|
|
23
|
+
if (!KINDS.has(kind)) throw typedError('EBADINPUT', 'custom action ' + id + ' kind must be "cli" or "mcp"');
|
|
24
|
+
const action = {
|
|
25
|
+
id,
|
|
26
|
+
label: cleanText(raw.label, 120) || id,
|
|
27
|
+
description: cleanText(raw.description, 500),
|
|
28
|
+
kind
|
|
29
|
+
};
|
|
30
|
+
if (kind === 'cli') {
|
|
31
|
+
action.command = cleanText(raw.command, 16 * 1024);
|
|
32
|
+
if (!action.command) throw typedError('EBADINPUT', 'custom action ' + id + ' command is required');
|
|
33
|
+
if (Number.isFinite(raw.timeoutMs)) action.timeoutMs = Math.max(1, Math.min(600000, Math.round(raw.timeoutMs)));
|
|
34
|
+
} else {
|
|
35
|
+
action.serverId = cleanText(raw.serverId, 256);
|
|
36
|
+
action.toolName = cleanText(raw.toolName, 256);
|
|
37
|
+
if (!action.serverId) throw typedError('EBADINPUT', 'custom action ' + id + ' serverId is required');
|
|
38
|
+
if (!action.toolName) throw typedError('EBADINPUT', 'custom action ' + id + ' toolName is required');
|
|
39
|
+
action.args = isPlainObject(raw.args) ? raw.args : {};
|
|
40
|
+
}
|
|
41
|
+
return action;
|
|
42
|
+
}
|
|
43
|
+
function normalizeActions(raw) {
|
|
44
|
+
if (raw == null) return [];
|
|
45
|
+
if (!Array.isArray(raw)) throw typedError('EBADINPUT', 'customActions must be an array');
|
|
46
|
+
if (raw.length > 100) throw typedError('EBADINPUT', 'customActions is limited to 100 entries');
|
|
47
|
+
const actions = raw.map(normalizeAction);
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const action of actions) {
|
|
50
|
+
const key = action.id.toLowerCase();
|
|
51
|
+
if (seen.has(key)) throw typedError('EBADINPUT', 'duplicate custom action id: ' + action.id);
|
|
52
|
+
seen.add(key);
|
|
53
|
+
}
|
|
54
|
+
return actions;
|
|
55
|
+
}
|
|
56
|
+
function listActions(projectDir) {
|
|
57
|
+
const project = settings.getProject(projectDir);
|
|
58
|
+
return normalizeActions(project && project.customActions);
|
|
59
|
+
}
|
|
60
|
+
function replaceActions(projectDir, raw) {
|
|
61
|
+
const actions = normalizeActions(raw);
|
|
62
|
+
settings.setProject(projectDir, { customActions: actions });
|
|
63
|
+
return actions;
|
|
64
|
+
}
|
|
65
|
+
module.exports = { ID_RE, normalizeAction, normalizeActions, listActions, replaceActions };
|