@ww_nero/mini-cli 1.0.80 → 1.0.82

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.
@@ -1,77 +1,77 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
-
5
- const MINI_DIR_NAME = '.mini';
6
- const SETTINGS_FILE_NAME = 'settings.json';
7
- const DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT = 32768;
8
-
9
- const ensureArrayOfStrings = (value, fallback = []) => {
10
- if (!Array.isArray(value)) {
11
- return [...fallback];
12
- }
13
- return value.map(String).filter(Boolean);
14
- };
15
-
16
- const loadSettings = ({ defaultTools = [] } = {}) => {
17
- const settingsPath = path.join(os.homedir(), MINI_DIR_NAME, SETTINGS_FILE_NAME);
18
- const dir = path.dirname(settingsPath);
19
-
20
- if (!fs.existsSync(dir)) {
21
- fs.mkdirSync(dir, { recursive: true });
22
- }
23
-
24
- const defaultSettings = {
25
- mcps: [],
26
- tools: [...defaultTools],
27
- toolOutputTokenLimit: DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
28
- };
29
-
30
- let parsed = null;
31
- let created = false;
32
- let needsUpdate = false;
33
-
34
- if (!fs.existsSync(settingsPath)) {
35
- fs.writeFileSync(settingsPath, `${JSON.stringify(defaultSettings, null, 2)}\n`, 'utf8');
36
- parsed = defaultSettings;
37
- created = true;
38
- } else {
39
- try {
40
- const raw = fs.readFileSync(settingsPath, 'utf8') || '{}';
41
- parsed = JSON.parse(raw);
42
-
43
- if (typeof parsed.toolOutputTokenLimit !== 'number') {
44
- parsed.toolOutputTokenLimit = DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT;
45
- needsUpdate = true;
46
- }
47
- } catch (error) {
48
- parsed = defaultSettings;
49
- }
50
- }
51
-
52
- const settings = {
53
- mcps: ensureArrayOfStrings(parsed.mcps, defaultSettings.mcps),
54
- tools: ensureArrayOfStrings(parsed.tools, defaultSettings.tools),
55
- toolOutputTokenLimit: typeof parsed.toolOutputTokenLimit === 'number' && parsed.toolOutputTokenLimit > 0
56
- ? parsed.toolOutputTokenLimit
57
- : DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
58
- };
59
-
60
- if (needsUpdate) {
61
- try {
62
- fs.writeFileSync(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
63
- } catch (error) {
64
- }
65
- }
66
-
67
- return {
68
- settingsPath,
69
- settings,
70
- created
71
- };
72
- };
73
-
74
- module.exports = {
75
- loadSettings,
76
- DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
77
- };
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const MINI_DIR_NAME = '.mini';
6
+ const SETTINGS_FILE_NAME = 'settings.json';
7
+ const DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT = 32768;
8
+
9
+ const ensureArrayOfStrings = (value, fallback = []) => {
10
+ if (!Array.isArray(value)) {
11
+ return [...fallback];
12
+ }
13
+ return value.map(String).filter(Boolean);
14
+ };
15
+
16
+ const loadSettings = ({ defaultTools = [] } = {}) => {
17
+ const settingsPath = path.join(os.homedir(), MINI_DIR_NAME, SETTINGS_FILE_NAME);
18
+ const dir = path.dirname(settingsPath);
19
+
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+
24
+ const defaultSettings = {
25
+ mcps: [],
26
+ tools: [...defaultTools],
27
+ toolOutputTokenLimit: DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
28
+ };
29
+
30
+ let parsed = null;
31
+ let created = false;
32
+ let needsUpdate = false;
33
+
34
+ if (!fs.existsSync(settingsPath)) {
35
+ fs.writeFileSync(settingsPath, `${JSON.stringify(defaultSettings, null, 2)}\n`, 'utf8');
36
+ parsed = defaultSettings;
37
+ created = true;
38
+ } else {
39
+ try {
40
+ const raw = fs.readFileSync(settingsPath, 'utf8') || '{}';
41
+ parsed = JSON.parse(raw);
42
+
43
+ if (typeof parsed.toolOutputTokenLimit !== 'number') {
44
+ parsed.toolOutputTokenLimit = DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT;
45
+ needsUpdate = true;
46
+ }
47
+ } catch (error) {
48
+ parsed = defaultSettings;
49
+ }
50
+ }
51
+
52
+ const settings = {
53
+ mcps: ensureArrayOfStrings(parsed.mcps, defaultSettings.mcps),
54
+ tools: ensureArrayOfStrings(parsed.tools, defaultSettings.tools),
55
+ toolOutputTokenLimit: typeof parsed.toolOutputTokenLimit === 'number' && parsed.toolOutputTokenLimit > 0
56
+ ? parsed.toolOutputTokenLimit
57
+ : DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
58
+ };
59
+
60
+ if (needsUpdate) {
61
+ try {
62
+ fs.writeFileSync(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
63
+ } catch (error) {
64
+ }
65
+ }
66
+
67
+ return {
68
+ settingsPath,
69
+ settings,
70
+ created
71
+ };
72
+ };
73
+
74
+ module.exports = {
75
+ loadSettings,
76
+ DEFAULT_TOOL_OUTPUT_TOKEN_LIMIT
77
+ };
@@ -1,250 +1,250 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
-
5
- const MINI_DIR_NAME = '.mini';
6
- const SKILLS_DIR_NAME = 'skills';
7
- const SKILL_ENTRY_FILE = 'SKILL.md';
8
- const MAX_DESCRIPTION_LENGTH = 160;
9
-
10
- const getSkillsRoot = () => path.join(os.homedir(), MINI_DIR_NAME, SKILLS_DIR_NAME);
11
-
12
- const toDisplayPath = (targetPath = '') => {
13
- const absolutePath = path.resolve(String(targetPath || ''));
14
- const homePath = path.resolve(os.homedir());
15
- const relativePath = path.relative(homePath, absolutePath);
16
-
17
- if (relativePath === '') {
18
- return '~';
19
- }
20
-
21
- if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
22
- return `~/${relativePath.split(path.sep).join('/')}`;
23
- }
24
-
25
- return absolutePath;
26
- };
27
-
28
- const isPathInside = (basePath, targetPath) => {
29
- const relativePath = path.relative(path.resolve(basePath), path.resolve(targetPath));
30
- return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
31
- };
32
-
33
- const trimDescription = (text = '') => {
34
- const normalized = String(text || '').replace(/\s+/g, ' ').trim();
35
- if (!normalized) {
36
- return '';
37
- }
38
- if (normalized.length <= MAX_DESCRIPTION_LENGTH) {
39
- return normalized;
40
- }
41
- return `${normalized.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
42
- };
43
-
44
- const extractDescription = (content = '') => {
45
- const normalized = String(content || '').replace(/\r/g, '');
46
- if (!normalized.trim()) {
47
- return '';
48
- }
49
-
50
- const taggedPattern = /^\s*(?:description|desc|简介|说明)\s*[::]\s*(.+)$/im;
51
- const taggedMatch = normalized.match(taggedPattern);
52
- if (taggedMatch && taggedMatch[1]) {
53
- return trimDescription(taggedMatch[1]);
54
- }
55
-
56
- const lines = normalized.split('\n');
57
- let inCodeBlock = false;
58
-
59
- for (const rawLine of lines) {
60
- const line = rawLine.trim();
61
-
62
- if (line.startsWith('```')) {
63
- inCodeBlock = !inCodeBlock;
64
- continue;
65
- }
66
-
67
- if (!line || inCodeBlock) {
68
- continue;
69
- }
70
-
71
- if (
72
- line.startsWith('#')
73
- || line.startsWith('-')
74
- || line.startsWith('*')
75
- || line.startsWith('>')
76
- || /^\d+[.)]\s/.test(line)
77
- ) {
78
- continue;
79
- }
80
-
81
- return trimDescription(line);
82
- }
83
-
84
- return '';
85
- };
86
-
87
- const discoverSkills = () => {
88
- const skillsRoot = getSkillsRoot();
89
-
90
- if (!fs.existsSync(skillsRoot)) {
91
- return {
92
- skillsRoot,
93
- skills: []
94
- };
95
- }
96
-
97
- let entries = [];
98
-
99
- try {
100
- entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
101
- } catch (error) {
102
- return {
103
- skillsRoot,
104
- skills: [],
105
- error: `读取 skills 目录失败:${error.message}`
106
- };
107
- }
108
-
109
- const skills = entries
110
- .filter((entry) => entry && entry.isDirectory() && !entry.name.startsWith('.'))
111
- .map((entry) => {
112
- const skillDirPath = path.join(skillsRoot, entry.name);
113
- const entryFilePath = path.join(skillDirPath, SKILL_ENTRY_FILE);
114
-
115
- if (!fs.existsSync(entryFilePath)) {
116
- return null;
117
- }
118
-
119
- let description = '';
120
- try {
121
- const content = fs.readFileSync(entryFilePath, 'utf8');
122
- description = extractDescription(content);
123
- } catch (_) {
124
- description = '';
125
- }
126
-
127
- return {
128
- name: entry.name,
129
- description,
130
- dirPath: skillDirPath,
131
- entryFilePath
132
- };
133
- })
134
- .filter(Boolean)
135
- .sort((left, right) => left.name.localeCompare(right.name));
136
-
137
- return {
138
- skillsRoot,
139
- skills
140
- };
141
- };
142
-
143
- const resolveSkillDirectory = (skillsRoot, skillName) => {
144
- if (typeof skillName !== 'string' || !skillName.trim()) {
145
- throw new Error('skillName 参数不能为空');
146
- }
147
-
148
- const normalizedName = skillName.trim();
149
- const skillDirPath = path.resolve(skillsRoot, normalizedName);
150
-
151
- if (!isPathInside(skillsRoot, skillDirPath)) {
152
- throw new Error('skillName 非法,不能超出 skills 目录');
153
- }
154
-
155
- if (path.resolve(path.dirname(skillDirPath)) !== path.resolve(skillsRoot)) {
156
- throw new Error('skillName 必须是 skills 目录下的直接子目录名');
157
- }
158
-
159
- if (!fs.existsSync(skillDirPath) || !fs.statSync(skillDirPath).isDirectory()) {
160
- throw new Error(`skill 不存在: ${normalizedName}`);
161
- }
162
-
163
- return {
164
- skillName: normalizedName,
165
- skillDirPath
166
- };
167
- };
168
-
169
- const resolveSkillFilePath = (skillDirPath, filePath = SKILL_ENTRY_FILE) => {
170
- const normalizedFilePath = typeof filePath === 'string' && filePath.trim()
171
- ? filePath.trim()
172
- : SKILL_ENTRY_FILE;
173
-
174
- if (path.isAbsolute(normalizedFilePath)) {
175
- throw new Error('filePath 必须是相对于 skill 目录的路径');
176
- }
177
-
178
- const absoluteFilePath = path.resolve(skillDirPath, normalizedFilePath);
179
-
180
- if (!isPathInside(skillDirPath, absoluteFilePath)) {
181
- throw new Error('filePath 非法,不能超出 skill 目录');
182
- }
183
-
184
- if (!fs.existsSync(absoluteFilePath) || !fs.statSync(absoluteFilePath).isFile()) {
185
- throw new Error(`文件不存在: ${normalizedFilePath}`);
186
- }
187
-
188
- const relativeFilePath = path.relative(skillDirPath, absoluteFilePath).split(path.sep).join('/');
189
-
190
- return {
191
- absoluteFilePath,
192
- relativeFilePath
193
- };
194
- };
195
-
196
- const readSkillFile = ({ skillName, filePath } = {}) => {
197
- const skillsRoot = getSkillsRoot();
198
- if (!fs.existsSync(skillsRoot)) {
199
- throw new Error(`skills 目录不存在: ${toDisplayPath(skillsRoot)}`);
200
- }
201
-
202
- const { skillName: normalizedSkillName, skillDirPath } = resolveSkillDirectory(skillsRoot, skillName);
203
- const { absoluteFilePath, relativeFilePath } = resolveSkillFilePath(skillDirPath, filePath);
204
- const content = fs.readFileSync(absoluteFilePath, 'utf8');
205
-
206
- return {
207
- skillsRoot,
208
- skillName: normalizedSkillName,
209
- skillDirPath,
210
- filePath: relativeFilePath,
211
- absoluteFilePath,
212
- content
213
- };
214
- };
215
-
216
- const buildSkillsSystemPrompt = (snapshot = {}) => {
217
- const skills = Array.isArray(snapshot.skills) ? snapshot.skills : [];
218
- if (skills.length === 0) {
219
- return '';
220
- }
221
-
222
- const lines = [
223
- '<skills>',
224
- `skills 目录: ${toDisplayPath(snapshot.skillsRoot || getSkillsRoot())}`,
225
- '当用户明确提到某个 skill,或任务明显匹配某个 skill 描述时,优先使用该 skill。',
226
- 'skill 使用流程:',
227
- '1. 先调用 skills 工具读取对应 skill 的 SKILL.md。',
228
- '2. 若 SKILL.md 引用其他文件,再调用 skills 工具按需读取。',
229
- '3. 仅加载当前任务需要的内容,避免无关读取。',
230
- '可用 skills:'
231
- ];
232
-
233
- skills.forEach((skill) => {
234
- const description = skill.description || '(暂无描述)';
235
- lines.push(`- ${skill.name}: ${description}`);
236
- });
237
-
238
- lines.push('</skills>');
239
-
240
- return lines.join('\n');
241
- };
242
-
243
- module.exports = {
244
- SKILL_ENTRY_FILE,
245
- getSkillsRoot,
246
- discoverSkills,
247
- readSkillFile,
248
- toDisplayPath,
249
- buildSkillsSystemPrompt
250
- };
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const MINI_DIR_NAME = '.mini';
6
+ const SKILLS_DIR_NAME = 'skills';
7
+ const SKILL_ENTRY_FILE = 'SKILL.md';
8
+ const MAX_DESCRIPTION_LENGTH = 160;
9
+
10
+ const getSkillsRoot = () => path.join(os.homedir(), MINI_DIR_NAME, SKILLS_DIR_NAME);
11
+
12
+ const toDisplayPath = (targetPath = '') => {
13
+ const absolutePath = path.resolve(String(targetPath || ''));
14
+ const homePath = path.resolve(os.homedir());
15
+ const relativePath = path.relative(homePath, absolutePath);
16
+
17
+ if (relativePath === '') {
18
+ return '~';
19
+ }
20
+
21
+ if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
22
+ return `~/${relativePath.split(path.sep).join('/')}`;
23
+ }
24
+
25
+ return absolutePath;
26
+ };
27
+
28
+ const isPathInside = (basePath, targetPath) => {
29
+ const relativePath = path.relative(path.resolve(basePath), path.resolve(targetPath));
30
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
31
+ };
32
+
33
+ const trimDescription = (text = '') => {
34
+ const normalized = String(text || '').replace(/\s+/g, ' ').trim();
35
+ if (!normalized) {
36
+ return '';
37
+ }
38
+ if (normalized.length <= MAX_DESCRIPTION_LENGTH) {
39
+ return normalized;
40
+ }
41
+ return `${normalized.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
42
+ };
43
+
44
+ const extractDescription = (content = '') => {
45
+ const normalized = String(content || '').replace(/\r/g, '');
46
+ if (!normalized.trim()) {
47
+ return '';
48
+ }
49
+
50
+ const taggedPattern = /^\s*(?:description|desc|简介|说明)\s*[::]\s*(.+)$/im;
51
+ const taggedMatch = normalized.match(taggedPattern);
52
+ if (taggedMatch && taggedMatch[1]) {
53
+ return trimDescription(taggedMatch[1]);
54
+ }
55
+
56
+ const lines = normalized.split('\n');
57
+ let inCodeBlock = false;
58
+
59
+ for (const rawLine of lines) {
60
+ const line = rawLine.trim();
61
+
62
+ if (line.startsWith('```')) {
63
+ inCodeBlock = !inCodeBlock;
64
+ continue;
65
+ }
66
+
67
+ if (!line || inCodeBlock) {
68
+ continue;
69
+ }
70
+
71
+ if (
72
+ line.startsWith('#')
73
+ || line.startsWith('-')
74
+ || line.startsWith('*')
75
+ || line.startsWith('>')
76
+ || /^\d+[.)]\s/.test(line)
77
+ ) {
78
+ continue;
79
+ }
80
+
81
+ return trimDescription(line);
82
+ }
83
+
84
+ return '';
85
+ };
86
+
87
+ const discoverSkills = () => {
88
+ const skillsRoot = getSkillsRoot();
89
+
90
+ if (!fs.existsSync(skillsRoot)) {
91
+ return {
92
+ skillsRoot,
93
+ skills: []
94
+ };
95
+ }
96
+
97
+ let entries = [];
98
+
99
+ try {
100
+ entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
101
+ } catch (error) {
102
+ return {
103
+ skillsRoot,
104
+ skills: [],
105
+ error: `读取 skills 目录失败:${error.message}`
106
+ };
107
+ }
108
+
109
+ const skills = entries
110
+ .filter((entry) => entry && entry.isDirectory() && !entry.name.startsWith('.'))
111
+ .map((entry) => {
112
+ const skillDirPath = path.join(skillsRoot, entry.name);
113
+ const entryFilePath = path.join(skillDirPath, SKILL_ENTRY_FILE);
114
+
115
+ if (!fs.existsSync(entryFilePath)) {
116
+ return null;
117
+ }
118
+
119
+ let description = '';
120
+ try {
121
+ const content = fs.readFileSync(entryFilePath, 'utf8');
122
+ description = extractDescription(content);
123
+ } catch (_) {
124
+ description = '';
125
+ }
126
+
127
+ return {
128
+ name: entry.name,
129
+ description,
130
+ dirPath: skillDirPath,
131
+ entryFilePath
132
+ };
133
+ })
134
+ .filter(Boolean)
135
+ .sort((left, right) => left.name.localeCompare(right.name));
136
+
137
+ return {
138
+ skillsRoot,
139
+ skills
140
+ };
141
+ };
142
+
143
+ const resolveSkillDirectory = (skillsRoot, skillName) => {
144
+ if (typeof skillName !== 'string' || !skillName.trim()) {
145
+ throw new Error('skillName 参数不能为空');
146
+ }
147
+
148
+ const normalizedName = skillName.trim();
149
+ const skillDirPath = path.resolve(skillsRoot, normalizedName);
150
+
151
+ if (!isPathInside(skillsRoot, skillDirPath)) {
152
+ throw new Error('skillName 非法,不能超出 skills 目录');
153
+ }
154
+
155
+ if (path.resolve(path.dirname(skillDirPath)) !== path.resolve(skillsRoot)) {
156
+ throw new Error('skillName 必须是 skills 目录下的直接子目录名');
157
+ }
158
+
159
+ if (!fs.existsSync(skillDirPath) || !fs.statSync(skillDirPath).isDirectory()) {
160
+ throw new Error(`skill 不存在: ${normalizedName}`);
161
+ }
162
+
163
+ return {
164
+ skillName: normalizedName,
165
+ skillDirPath
166
+ };
167
+ };
168
+
169
+ const resolveSkillFilePath = (skillDirPath, filePath = SKILL_ENTRY_FILE) => {
170
+ const normalizedFilePath = typeof filePath === 'string' && filePath.trim()
171
+ ? filePath.trim()
172
+ : SKILL_ENTRY_FILE;
173
+
174
+ if (path.isAbsolute(normalizedFilePath)) {
175
+ throw new Error('filePath 必须是相对于 skill 目录的路径');
176
+ }
177
+
178
+ const absoluteFilePath = path.resolve(skillDirPath, normalizedFilePath);
179
+
180
+ if (!isPathInside(skillDirPath, absoluteFilePath)) {
181
+ throw new Error('filePath 非法,不能超出 skill 目录');
182
+ }
183
+
184
+ if (!fs.existsSync(absoluteFilePath) || !fs.statSync(absoluteFilePath).isFile()) {
185
+ throw new Error(`文件不存在: ${normalizedFilePath}`);
186
+ }
187
+
188
+ const relativeFilePath = path.relative(skillDirPath, absoluteFilePath).split(path.sep).join('/');
189
+
190
+ return {
191
+ absoluteFilePath,
192
+ relativeFilePath
193
+ };
194
+ };
195
+
196
+ const readSkillFile = ({ skillName, filePath } = {}) => {
197
+ const skillsRoot = getSkillsRoot();
198
+ if (!fs.existsSync(skillsRoot)) {
199
+ throw new Error(`skills 目录不存在: ${toDisplayPath(skillsRoot)}`);
200
+ }
201
+
202
+ const { skillName: normalizedSkillName, skillDirPath } = resolveSkillDirectory(skillsRoot, skillName);
203
+ const { absoluteFilePath, relativeFilePath } = resolveSkillFilePath(skillDirPath, filePath);
204
+ const content = fs.readFileSync(absoluteFilePath, 'utf8');
205
+
206
+ return {
207
+ skillsRoot,
208
+ skillName: normalizedSkillName,
209
+ skillDirPath,
210
+ filePath: relativeFilePath,
211
+ absoluteFilePath,
212
+ content
213
+ };
214
+ };
215
+
216
+ const buildSkillsSystemPrompt = (snapshot = {}) => {
217
+ const skills = Array.isArray(snapshot.skills) ? snapshot.skills : [];
218
+ if (skills.length === 0) {
219
+ return '';
220
+ }
221
+
222
+ const lines = [
223
+ '<skills>',
224
+ `skills 目录: ${toDisplayPath(snapshot.skillsRoot || getSkillsRoot())}`,
225
+ '当用户明确提到某个 skill,或任务明显匹配某个 skill 描述时,优先使用该 skill。',
226
+ 'skill 使用流程:',
227
+ '1. 先调用 skills 工具读取对应 skill 的 SKILL.md。',
228
+ '2. 若 SKILL.md 引用其他文件,再调用 skills 工具按需读取。',
229
+ '3. 仅加载当前任务需要的内容,避免无关读取。',
230
+ '可用 skills:'
231
+ ];
232
+
233
+ skills.forEach((skill) => {
234
+ const description = skill.description || '(暂无描述)';
235
+ lines.push(`- ${skill.name}: ${description}`);
236
+ });
237
+
238
+ lines.push('</skills>');
239
+
240
+ return lines.join('\n');
241
+ };
242
+
243
+ module.exports = {
244
+ SKILL_ENTRY_FILE,
245
+ getSkillsRoot,
246
+ discoverSkills,
247
+ readSkillFile,
248
+ toDisplayPath,
249
+ buildSkillsSystemPrompt
250
+ };