mini-coder 0.5.14 → 0.6.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/README.md +25 -109
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/package.json +17 -22
- package/src/agent.ts +237 -1403
- package/src/args.ts +289 -0
- package/src/headless.ts +43 -358
- package/src/index.ts +29 -1016
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +227 -284
- package/src/session.ts +55 -1306
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-task.ts +114 -0
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +262 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +236 -0
- package/src/types.ts +160 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -5
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/assistant-output.ts +0 -73
- package/src/cli.ts +0 -134
- package/src/delegation.ts +0 -238
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -385
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -376
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-delegate.ts +0 -125
- package/src/tool-grep.ts +0 -606
- package/src/tool-read.ts +0 -313
- package/src/tool-shell.ts +0 -1051
- package/src/tools.ts +0 -1179
- package/src/ui/agent.ts +0 -320
- package/src/ui/commands.test.ts +0 -957
- package/src/ui/commands.ts +0 -848
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/settings.ts
DELETED
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* User settings persistence and startup resolution.
|
|
3
|
-
*
|
|
4
|
-
* Stores global defaults such as model, effort, reasoning visibility,
|
|
5
|
-
* verbose tool output, custom providers, and MCP server settings in a JSON
|
|
6
|
-
* file under the app data directory.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
|
-
import type { ThinkingLevel } from "@mariozechner/pi-ai";
|
|
14
|
-
import { getErrorMessage } from "./errors.ts";
|
|
15
|
-
import { readBoolean, readString, toRecord } from "./shared.ts";
|
|
16
|
-
|
|
17
|
-
/** A user-configured OpenAI-compatible provider endpoint. */
|
|
18
|
-
export interface CustomProvider {
|
|
19
|
-
/** Provider identifier, e.g. "ollama". Shown as the provider prefix in model names. */
|
|
20
|
-
name: string;
|
|
21
|
-
/** OpenAI-compatible API base URL, e.g. "http://localhost:11434/v1". */
|
|
22
|
-
baseUrl: string;
|
|
23
|
-
/** Optional API key. Defaults to "no-key" at discovery time. */
|
|
24
|
-
apiKey?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A single configured MCP server endpoint. */
|
|
28
|
-
export interface McpServerConfig {
|
|
29
|
-
/** Stable server identifier. Used as the imported tool-name prefix. */
|
|
30
|
-
name: string;
|
|
31
|
-
/** Absolute Streamable HTTP MCP endpoint URL. */
|
|
32
|
-
url: string;
|
|
33
|
-
/** Whether the server should start enabled. */
|
|
34
|
-
enabled: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** MCP-related user settings. */
|
|
38
|
-
export interface McpSettings {
|
|
39
|
-
/** MCP servers to connect to at startup. */
|
|
40
|
-
servers?: McpServerConfig[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Default reasoning effort when no saved setting exists. */
|
|
44
|
-
const DEFAULT_EFFORT: ThinkingLevel = "medium";
|
|
45
|
-
|
|
46
|
-
/** Default reasoning visibility when no saved setting exists. */
|
|
47
|
-
export const DEFAULT_SHOW_REASONING = true;
|
|
48
|
-
|
|
49
|
-
/** Default verbose tool rendering flag when no saved setting exists. */
|
|
50
|
-
export const DEFAULT_VERBOSE = false;
|
|
51
|
-
|
|
52
|
-
/** Persisted global user settings. */
|
|
53
|
-
export interface UserSettings {
|
|
54
|
-
/** Preferred provider/model identifier. */
|
|
55
|
-
defaultModel?: string;
|
|
56
|
-
/** Preferred reasoning effort. */
|
|
57
|
-
defaultEffort?: ThinkingLevel;
|
|
58
|
-
/** Whether reasoning blocks are shown in the UI. */
|
|
59
|
-
showReasoning?: boolean;
|
|
60
|
-
/** Whether full tool output is shown in the UI. */
|
|
61
|
-
verbose?: boolean;
|
|
62
|
-
/** Custom OpenAI-compatible provider endpoints. */
|
|
63
|
-
customProviders?: CustomProvider[];
|
|
64
|
-
/** MCP server definitions to manage and connect when enabled. */
|
|
65
|
-
mcp?: McpSettings;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Resolved startup settings after applying defaults and availability checks. */
|
|
69
|
-
interface StartupSettings {
|
|
70
|
-
/** Model to use for this launch, or `null` if none are available. */
|
|
71
|
-
modelId: string | null;
|
|
72
|
-
/** Effective reasoning effort. */
|
|
73
|
-
effort: ThinkingLevel;
|
|
74
|
-
/** Effective reasoning visibility. */
|
|
75
|
-
showReasoning: boolean;
|
|
76
|
-
/** Effective verbose flag. */
|
|
77
|
-
verbose: boolean;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const THINKING_LEVELS = new Set<ThinkingLevel>([
|
|
81
|
-
"low",
|
|
82
|
-
"medium",
|
|
83
|
-
"high",
|
|
84
|
-
"xhigh",
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Load and validate user settings from disk.
|
|
91
|
-
*
|
|
92
|
-
* Missing files are treated as empty settings. Invalid JSON or unreadable files
|
|
93
|
-
* fail with a descriptive error instead of silently discarding saved state.
|
|
94
|
-
*
|
|
95
|
-
* @param path - Absolute path to `settings.json`.
|
|
96
|
-
* @returns The validated settings object.
|
|
97
|
-
*/
|
|
98
|
-
export function loadSettings(path: string): UserSettings {
|
|
99
|
-
if (!existsSync(path)) {
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
return parseSettingsFile(path);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
throw createSettingsReadError(path, error);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Load settings for startup without aborting on invalid JSON.
|
|
112
|
-
*
|
|
113
|
-
* Missing files and invalid JSON content are treated as empty settings so
|
|
114
|
-
* startup behaves like there are no saved settings. Other filesystem errors
|
|
115
|
-
* still fail with the same descriptive read error as {@link loadSettings}.
|
|
116
|
-
*
|
|
117
|
-
* @param path - Absolute path to `settings.json`.
|
|
118
|
-
* @returns The validated settings object, or `{}` when startup should ignore invalid JSON.
|
|
119
|
-
*/
|
|
120
|
-
export function loadStartupSettings(path: string): UserSettings {
|
|
121
|
-
if (!existsSync(path)) {
|
|
122
|
-
return {};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
return parseSettingsFile(path);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error instanceof SyntaxError) {
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
throw createSettingsReadError(path, error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Merge two settings objects using startup overlay semantics.
|
|
137
|
-
*
|
|
138
|
-
* Scalar fields use override-wins. `customProviders` merges by provider name,
|
|
139
|
-
* and `mcp.servers` merges by server name. Same-name override entries replace
|
|
140
|
-
* base entries while keeping the base ordering stable; new override entries are
|
|
141
|
-
* appended in override order.
|
|
142
|
-
*
|
|
143
|
-
* @param base - Base settings, usually the global settings file.
|
|
144
|
-
* @param override - Higher-priority settings, usually a repo-local overlay.
|
|
145
|
-
* @returns The merged effective settings.
|
|
146
|
-
*/
|
|
147
|
-
export function mergeUserSettings(
|
|
148
|
-
base: UserSettings,
|
|
149
|
-
override: UserSettings,
|
|
150
|
-
): UserSettings {
|
|
151
|
-
const sanitizedBase = sanitizeSettings(base);
|
|
152
|
-
const sanitizedOverride = sanitizeSettings(override);
|
|
153
|
-
const merged: UserSettings = {
|
|
154
|
-
...sanitizedBase,
|
|
155
|
-
...sanitizedOverride,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const customProviders = mergeNamedEntries(
|
|
159
|
-
sanitizedBase.customProviders,
|
|
160
|
-
sanitizedOverride.customProviders,
|
|
161
|
-
);
|
|
162
|
-
if (customProviders) {
|
|
163
|
-
merged.customProviders = customProviders;
|
|
164
|
-
} else {
|
|
165
|
-
delete merged.customProviders;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const servers = mergeNamedEntries(
|
|
169
|
-
sanitizedBase.mcp?.servers,
|
|
170
|
-
sanitizedOverride.mcp?.servers,
|
|
171
|
-
);
|
|
172
|
-
if (servers) {
|
|
173
|
-
merged.mcp = { servers };
|
|
174
|
-
} else {
|
|
175
|
-
delete merged.mcp;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return merged;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Save user settings to disk.
|
|
183
|
-
*
|
|
184
|
-
* Parent directories are created automatically. Only validated fields are
|
|
185
|
-
* written to disk.
|
|
186
|
-
*
|
|
187
|
-
* @param path - Absolute path to `settings.json`.
|
|
188
|
-
* @param settings - Settings to persist.
|
|
189
|
-
* @returns The validated settings that were written.
|
|
190
|
-
*/
|
|
191
|
-
export function saveSettings(
|
|
192
|
-
path: string,
|
|
193
|
-
settings: UserSettings,
|
|
194
|
-
): UserSettings {
|
|
195
|
-
const sanitized = sanitizeSettings(settings);
|
|
196
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
197
|
-
writeFileSync(path, JSON.stringify(sanitized, null, 2), "utf-8");
|
|
198
|
-
return sanitized;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Merge a partial settings update with the current file contents and persist it.
|
|
203
|
-
*
|
|
204
|
-
* Invalid fields in the update are ignored.
|
|
205
|
-
*
|
|
206
|
-
* @param path - Absolute path to `settings.json`.
|
|
207
|
-
* @param update - Partial settings update to merge.
|
|
208
|
-
* @returns The merged settings after persistence.
|
|
209
|
-
*/
|
|
210
|
-
export function updateSettings(
|
|
211
|
-
path: string,
|
|
212
|
-
update: Partial<UserSettings>,
|
|
213
|
-
): UserSettings {
|
|
214
|
-
const current = loadSettings(path);
|
|
215
|
-
const merged = mergeUserSettings(current, sanitizeSettings(update));
|
|
216
|
-
return saveSettings(path, merged);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Resolve the effective startup settings for the current launch.
|
|
221
|
-
*
|
|
222
|
-
* The saved preferred model is only used when it is currently available.
|
|
223
|
-
* Otherwise the first available model is used for this launch, while the saved
|
|
224
|
-
* preference remains unchanged on disk.
|
|
225
|
-
*
|
|
226
|
-
* @param settings - Saved user settings.
|
|
227
|
-
* @param availableModelIds - Provider/model identifiers available this launch.
|
|
228
|
-
* @returns Effective startup settings with fallbacks applied.
|
|
229
|
-
*/
|
|
230
|
-
export function resolveStartupSettings(
|
|
231
|
-
settings: UserSettings,
|
|
232
|
-
availableModelIds: readonly string[],
|
|
233
|
-
): StartupSettings {
|
|
234
|
-
const preferredModel = settings.defaultModel;
|
|
235
|
-
const modelId =
|
|
236
|
-
preferredModel && availableModelIds.includes(preferredModel)
|
|
237
|
-
? preferredModel
|
|
238
|
-
: (availableModelIds[0] ?? null);
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
modelId,
|
|
242
|
-
effort: settings.defaultEffort ?? DEFAULT_EFFORT,
|
|
243
|
-
showReasoning: settings.showReasoning ?? DEFAULT_SHOW_REASONING,
|
|
244
|
-
verbose: settings.verbose ?? DEFAULT_VERBOSE,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function mergeNamedEntries<T extends { name: string }>(
|
|
249
|
-
base: readonly T[] | undefined,
|
|
250
|
-
override: readonly T[] | undefined,
|
|
251
|
-
): T[] | undefined {
|
|
252
|
-
if (!base?.length && !override?.length) {
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const merged = [...(base ?? [])];
|
|
257
|
-
const indexes = new Map<string, number>();
|
|
258
|
-
|
|
259
|
-
for (const [index, entry] of merged.entries()) {
|
|
260
|
-
indexes.set(entry.name, index);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
for (const entry of override ?? []) {
|
|
264
|
-
const existingIndex = indexes.get(entry.name);
|
|
265
|
-
if (existingIndex === undefined) {
|
|
266
|
-
indexes.set(entry.name, merged.length);
|
|
267
|
-
merged.push(entry);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
merged[existingIndex] = entry;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return merged.length > 0 ? merged : undefined;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function parseSettingsFile(path: string): UserSettings {
|
|
278
|
-
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
279
|
-
return sanitizeSettings(raw);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function createSettingsReadError(path: string, error: unknown): Error {
|
|
283
|
-
return new Error(
|
|
284
|
-
`Failed to read settings ${path}: ${getErrorMessage(error)}`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Validate and normalize a parsed settings object.
|
|
290
|
-
*
|
|
291
|
-
* Unknown or invalid fields are dropped.
|
|
292
|
-
*
|
|
293
|
-
* @param value - Parsed JSON value.
|
|
294
|
-
* @returns Sanitized settings.
|
|
295
|
-
*/
|
|
296
|
-
function sanitizeSettings(value: unknown): UserSettings {
|
|
297
|
-
const candidate = toRecord(value);
|
|
298
|
-
if (!candidate) {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const settings: UserSettings = {};
|
|
303
|
-
const defaultModel = readString(candidate, "defaultModel");
|
|
304
|
-
const showReasoning = readBoolean(candidate, "showReasoning");
|
|
305
|
-
const verbose = readBoolean(candidate, "verbose");
|
|
306
|
-
|
|
307
|
-
if (defaultModel !== null) {
|
|
308
|
-
settings.defaultModel = defaultModel;
|
|
309
|
-
}
|
|
310
|
-
if (isThinkingLevel(candidate.defaultEffort)) {
|
|
311
|
-
settings.defaultEffort = candidate.defaultEffort;
|
|
312
|
-
}
|
|
313
|
-
if (showReasoning !== null) {
|
|
314
|
-
settings.showReasoning = showReasoning;
|
|
315
|
-
}
|
|
316
|
-
if (verbose !== null) {
|
|
317
|
-
settings.verbose = verbose;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const customProviders = sanitizeCustomProviders(candidate.customProviders);
|
|
321
|
-
if (customProviders) {
|
|
322
|
-
settings.customProviders = customProviders;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const mcp = sanitizeMcpSettings(candidate.mcp);
|
|
326
|
-
if (mcp) {
|
|
327
|
-
settings.mcp = mcp;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return settings;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** Try to parse a single custom provider entry, returning null on failure. */
|
|
334
|
-
function parseCustomProvider(item: unknown): CustomProvider | null {
|
|
335
|
-
const candidate = toRecord(item);
|
|
336
|
-
if (!candidate) {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const name = readString(candidate, "name")?.trim() ?? "";
|
|
341
|
-
const baseUrl = readString(candidate, "baseUrl")?.trim() ?? "";
|
|
342
|
-
|
|
343
|
-
if (!name || !baseUrl) {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const entry: CustomProvider = { name, baseUrl };
|
|
348
|
-
const apiKey = readString(candidate, "apiKey");
|
|
349
|
-
if (apiKey !== null) {
|
|
350
|
-
entry.apiKey = apiKey;
|
|
351
|
-
}
|
|
352
|
-
return entry;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Validate and normalize custom provider entries.
|
|
357
|
-
*
|
|
358
|
-
* Drops entries with missing/empty name or baseUrl, and deduplicates by name
|
|
359
|
-
* (first entry wins).
|
|
360
|
-
*/
|
|
361
|
-
function sanitizeCustomProviders(value: unknown): CustomProvider[] | undefined {
|
|
362
|
-
if (!Array.isArray(value)) {
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const result: CustomProvider[] = [];
|
|
367
|
-
const seen = new Set<string>();
|
|
368
|
-
|
|
369
|
-
for (const item of value) {
|
|
370
|
-
const entry = parseCustomProvider(item);
|
|
371
|
-
if (!entry || seen.has(entry.name)) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
seen.add(entry.name);
|
|
375
|
-
result.push(entry);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return result.length > 0 ? result : undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function sanitizeMcpSettings(value: unknown): McpSettings | undefined {
|
|
382
|
-
const candidate = toRecord(value);
|
|
383
|
-
if (!candidate) {
|
|
384
|
-
return undefined;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const servers = sanitizeMcpServers(candidate.servers);
|
|
388
|
-
if (!servers) {
|
|
389
|
-
return undefined;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return { servers };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Try to parse a single MCP server entry, returning null on failure. */
|
|
396
|
-
function parseMcpServer(item: unknown): McpServerConfig | null {
|
|
397
|
-
const candidate = toRecord(item);
|
|
398
|
-
if (!candidate) {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const name = readString(candidate, "name")?.trim() ?? "";
|
|
403
|
-
const url = readString(candidate, "url")?.trim() ?? "";
|
|
404
|
-
const enabled = readBoolean(candidate, "enabled") ?? true;
|
|
405
|
-
|
|
406
|
-
if (!name || !url || !MCP_SERVER_NAME_PATTERN.test(name)) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return { name, url, enabled };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Validate and normalize configured MCP servers.
|
|
415
|
-
*
|
|
416
|
-
* Drops entries with missing/invalid names or URLs, and deduplicates by name
|
|
417
|
-
* (first entry wins).
|
|
418
|
-
*/
|
|
419
|
-
function sanitizeMcpServers(value: unknown): McpServerConfig[] | undefined {
|
|
420
|
-
if (!Array.isArray(value)) {
|
|
421
|
-
return undefined;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const result: McpServerConfig[] = [];
|
|
425
|
-
const seen = new Set<string>();
|
|
426
|
-
|
|
427
|
-
for (const item of value) {
|
|
428
|
-
const entry = parseMcpServer(item);
|
|
429
|
-
if (!entry || seen.has(entry.name)) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
seen.add(entry.name);
|
|
433
|
-
result.push(entry);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return result.length > 0 ? result : undefined;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Check whether a value is a valid thinking level.
|
|
441
|
-
*
|
|
442
|
-
* @param value - Value to validate.
|
|
443
|
-
* @returns `true` when the value is a supported thinking level.
|
|
444
|
-
*/
|
|
445
|
-
function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
446
|
-
return (
|
|
447
|
-
typeof value === "string" && THINKING_LEVELS.has(value as ThinkingLevel)
|
|
448
|
-
);
|
|
449
|
-
}
|
package/src/skills.ts
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Skills (agentskills.io) discovery, parsing, and catalog generation.
|
|
3
|
-
*
|
|
4
|
-
* Scans configured directories for `SKILL.md` files, extracts YAML
|
|
5
|
-
* frontmatter (name, description), resolves name collisions (earlier
|
|
6
|
-
* scan paths win), and generates an XML catalog string for inclusion
|
|
7
|
-
* in the system prompt.
|
|
8
|
-
*
|
|
9
|
-
* @module
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Types
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A discovered agent skill.
|
|
21
|
-
*
|
|
22
|
-
* Represents a single SKILL.md file that has been parsed and is ready
|
|
23
|
-
* for inclusion in the system prompt catalog.
|
|
24
|
-
*/
|
|
25
|
-
export interface Skill {
|
|
26
|
-
/** Skill name (from frontmatter or directory name fallback). */
|
|
27
|
-
name: string;
|
|
28
|
-
/** Skill description (from frontmatter, empty if absent). */
|
|
29
|
-
description: string;
|
|
30
|
-
/** Absolute path to the SKILL.md file. */
|
|
31
|
-
path: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Frontmatter parsing
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
function getFrontmatterLines(content: string): string[] | null {
|
|
39
|
-
if (!content.startsWith("---")) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const endIdx = content.indexOf("\n---", 3);
|
|
44
|
-
if (endIdx === -1) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return content.slice(4, endIdx).split("\n");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseFrontmatterEntry(
|
|
52
|
-
line: string,
|
|
53
|
-
): { key: string; value: string } | null {
|
|
54
|
-
const colonIdx = line.indexOf(":");
|
|
55
|
-
if (colonIdx === -1) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
key: line.slice(0, colonIdx).trim(),
|
|
61
|
-
value: line.slice(colonIdx + 1).trim(),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isIndentedFrontmatterLine(line: string): boolean {
|
|
66
|
-
return line.startsWith(" ") || line.startsWith("\t");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readFoldedFrontmatterValue(
|
|
70
|
-
lines: string[],
|
|
71
|
-
startIndex: number,
|
|
72
|
-
): string {
|
|
73
|
-
const folded: string[] = [];
|
|
74
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
75
|
-
const line = lines[i];
|
|
76
|
-
if (!line || !isIndentedFrontmatterLine(line)) {
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
folded.push(line.trim());
|
|
80
|
-
}
|
|
81
|
-
return folded.join(" ");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function readDescriptionValue(
|
|
85
|
-
lines: string[],
|
|
86
|
-
lineIndex: number,
|
|
87
|
-
value: string,
|
|
88
|
-
): string {
|
|
89
|
-
if (value === ">") {
|
|
90
|
-
return readFoldedFrontmatterValue(lines, lineIndex + 1);
|
|
91
|
-
}
|
|
92
|
-
return stripQuotes(value);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Parse YAML frontmatter from a SKILL.md file's content.
|
|
97
|
-
*
|
|
98
|
-
* Handles simple key-value pairs, quoted strings, and YAML folded
|
|
99
|
-
* scalars (`>`). Does not use a full YAML parser — just enough to
|
|
100
|
-
* extract `name` and `description`.
|
|
101
|
-
*
|
|
102
|
-
* @param content - Raw file content.
|
|
103
|
-
* @returns Extracted name and description (both may be undefined).
|
|
104
|
-
*/
|
|
105
|
-
function parseFrontmatter(content: string): {
|
|
106
|
-
name: string | undefined;
|
|
107
|
-
description: string | undefined;
|
|
108
|
-
} {
|
|
109
|
-
const lines = getFrontmatterLines(content);
|
|
110
|
-
if (!lines) {
|
|
111
|
-
return { name: undefined, description: undefined };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let name: string | undefined;
|
|
115
|
-
let description: string | undefined;
|
|
116
|
-
|
|
117
|
-
for (const [index, line] of lines.entries()) {
|
|
118
|
-
const entry = parseFrontmatterEntry(line);
|
|
119
|
-
if (!entry) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (entry.key === "name") {
|
|
123
|
-
name = stripQuotes(entry.value);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (entry.key === "description") {
|
|
127
|
-
description = readDescriptionValue(lines, index, entry.value);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { name, description };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Strip surrounding single or double quotes from a string.
|
|
136
|
-
*
|
|
137
|
-
* @param s - The string to strip.
|
|
138
|
-
* @returns The string without surrounding quotes.
|
|
139
|
-
*/
|
|
140
|
-
function stripQuotes(s: string): string {
|
|
141
|
-
if (s.length >= 2) {
|
|
142
|
-
if (
|
|
143
|
-
(s[0] === '"' && s[s.length - 1] === '"') ||
|
|
144
|
-
(s[0] === "'" && s[s.length - 1] === "'")
|
|
145
|
-
) {
|
|
146
|
-
return s.slice(1, -1);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return s;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// Discovery
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
function listSkillEntries(basePath: string): string[] {
|
|
157
|
-
if (!existsSync(basePath)) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
return readdirSync(basePath);
|
|
162
|
-
} catch {
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isDirectory(path: string): boolean {
|
|
168
|
-
try {
|
|
169
|
-
return statSync(path).isDirectory();
|
|
170
|
-
} catch {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function readSkill(basePath: string, entry: string): Skill | null {
|
|
176
|
-
const dir = join(basePath, entry);
|
|
177
|
-
if (!isDirectory(dir)) {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const skillPath = join(dir, "SKILL.md");
|
|
182
|
-
if (!existsSync(skillPath)) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
188
|
-
const frontmatter = parseFrontmatter(content);
|
|
189
|
-
return {
|
|
190
|
-
name: frontmatter.name ?? entry,
|
|
191
|
-
description: frontmatter.description ?? "",
|
|
192
|
-
path: skillPath,
|
|
193
|
-
};
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Discover agent skills from the given scan paths.
|
|
201
|
-
*
|
|
202
|
-
* Scans each path for subdirectories containing a `SKILL.md` file.
|
|
203
|
-
* Earlier paths in the array have higher priority — on name collision,
|
|
204
|
-
* the first-seen skill wins (project-level over user-level).
|
|
205
|
-
*
|
|
206
|
-
* @param scanPaths - Ordered directories to scan (project paths first).
|
|
207
|
-
* @returns Array of discovered {@link Skill} records, deduplicated by name.
|
|
208
|
-
*/
|
|
209
|
-
export function discoverSkills(scanPaths: string[]): Skill[] {
|
|
210
|
-
const seen = new Map<string, Skill>();
|
|
211
|
-
|
|
212
|
-
for (const basePath of scanPaths) {
|
|
213
|
-
for (const entry of listSkillEntries(basePath)) {
|
|
214
|
-
const skill = readSkill(basePath, entry);
|
|
215
|
-
if (!skill || seen.has(skill.name)) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
seen.set(skill.name, skill);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return [...seen.values()];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// Catalog generation
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
function escapeXml(value: string): string {
|
|
230
|
-
return value
|
|
231
|
-
.replaceAll("&", "&")
|
|
232
|
-
.replaceAll("<", "<")
|
|
233
|
-
.replaceAll(">", ">")
|
|
234
|
-
.replaceAll('"', """)
|
|
235
|
-
.replaceAll("'", "'");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Build the XML skill catalog for the system prompt.
|
|
240
|
-
*
|
|
241
|
-
* Returns the full catalog section including the preamble text and
|
|
242
|
-
* `<available_skills>` XML block. Returns an empty string if no
|
|
243
|
-
* skills are provided.
|
|
244
|
-
*
|
|
245
|
-
* @param skills - Discovered skills to include.
|
|
246
|
-
* @returns The catalog string to append to the system prompt.
|
|
247
|
-
*/
|
|
248
|
-
export function buildSkillCatalog(skills: Skill[]): string {
|
|
249
|
-
if (skills.length === 0) return "";
|
|
250
|
-
|
|
251
|
-
const entries = skills
|
|
252
|
-
.map(
|
|
253
|
-
(s) =>
|
|
254
|
-
` <skill>\n` +
|
|
255
|
-
` <name>${escapeXml(s.name)}</name>\n` +
|
|
256
|
-
` <description>${escapeXml(s.description)}</description>\n` +
|
|
257
|
-
` <location>${escapeXml(s.path)}</location>\n` +
|
|
258
|
-
` </skill>`,
|
|
259
|
-
)
|
|
260
|
-
.join("\n");
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
`The following skills provide specialized instructions for specific tasks.\n` +
|
|
264
|
-
`Use the shell tool to read a skill's file when the task matches its description.\n` +
|
|
265
|
-
`When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.\n` +
|
|
266
|
-
`\n` +
|
|
267
|
-
`<available_skills>\n` +
|
|
268
|
-
entries +
|
|
269
|
-
`\n</available_skills>`
|
|
270
|
-
);
|
|
271
|
-
}
|