sp-rag 0.6.9 → 0.6.11
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/dist/cli.js +23 -9
- package/dist/lib/doctor.js +57 -22
- package/dist/lib/skill.js +15 -1
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -151,7 +151,7 @@ async function loadRuntimeDefaults(parsed) {
|
|
|
151
151
|
(explicitClient ? defaultSkillClientForMcpClient(explicitClient) : undefined) ??
|
|
152
152
|
config?.skillClient ??
|
|
153
153
|
derivedSkillClient,
|
|
154
|
-
skillTargetDir: optionString(parsed, 'target-dir')
|
|
154
|
+
skillTargetDir: optionString(parsed, 'target-dir'),
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
function resolveSelectedClient(parsed, defaults, explicitClient) {
|
|
@@ -177,8 +177,21 @@ function deriveDefaultsForClient(parsed, defaults, client) {
|
|
|
177
177
|
skillClient: nextSkillClient,
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
+
function validateMcpToken(token) {
|
|
181
|
+
const normalized = token?.trim();
|
|
182
|
+
if (!normalized) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (normalized.startsWith('pat_')) {
|
|
186
|
+
throw new Error('Token dạng pat_ không dùng được cho MCP. Đây có vẻ là token_id, không phải generated_token bí mật. Hãy dùng token bắt đầu bằng grc_pat_.');
|
|
187
|
+
}
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
|
190
|
+
function validateStoredToken(defaults) {
|
|
191
|
+
validateMcpToken(defaults.mcpToken);
|
|
192
|
+
}
|
|
180
193
|
function resolveAuthForMcp(parsed, defaults) {
|
|
181
|
-
const authToken = optionString(parsed, 'mcp-token') ?? defaults.mcpToken;
|
|
194
|
+
const authToken = validateMcpToken(optionString(parsed, 'mcp-token') ?? defaults.mcpToken);
|
|
182
195
|
const authEnvVar = optionString(parsed, 'auth-env-var') ?? defaults.authEnvVar;
|
|
183
196
|
if (!authToken?.trim() && !authEnvVar?.trim()) {
|
|
184
197
|
throw new Error('Chưa có token MCP trong config. Hãy chạy sp-rag install --client <client> --mcp-token <token> hoặc sp-rag token add --token <token>.');
|
|
@@ -223,7 +236,6 @@ function buildCliConfig(defaults) {
|
|
|
223
236
|
authEnvVar: defaults.authEnvVar,
|
|
224
237
|
mcpToken: defaults.mcpToken,
|
|
225
238
|
skillClient: defaults.skillClient,
|
|
226
|
-
skillTargetDir: defaults.skillTargetDir,
|
|
227
239
|
docsUrl: defaults.docsUrl,
|
|
228
240
|
};
|
|
229
241
|
}
|
|
@@ -373,8 +385,8 @@ async function runSkillInstall(parsed, defaults, explicitClient) {
|
|
|
373
385
|
const result = await installSkill({
|
|
374
386
|
client,
|
|
375
387
|
cwd: optionString(parsed, 'cwd'),
|
|
376
|
-
scope: supportedScope(optionString(parsed, 'scope'))
|
|
377
|
-
targetDir: optionString(parsed, 'target-dir')
|
|
388
|
+
scope: supportedScope(optionString(parsed, 'scope')),
|
|
389
|
+
targetDir: optionString(parsed, 'target-dir'),
|
|
378
390
|
serverAlias: optionString(parsed, 'server-alias') ?? defaults.serverAlias,
|
|
379
391
|
mcpUrl: optionString(parsed, 'mcp-url') ?? defaults.mcpUrl,
|
|
380
392
|
docsUrl: optionString(parsed, 'docs-url') ?? defaults.docsUrl,
|
|
@@ -411,6 +423,7 @@ async function emitDoctorReport(parsed, defaults, explicitClient, checks) {
|
|
|
411
423
|
}
|
|
412
424
|
async function runClientSetup(parsed, defaults, client) {
|
|
413
425
|
const nextDefaults = deriveDefaultsForClient(parsed, defaults, client);
|
|
426
|
+
validateStoredToken(nextDefaults);
|
|
414
427
|
const configPath = await saveResolvedConfig(nextDefaults);
|
|
415
428
|
process.stdout.write(`Đã lưu config CLI tại ${configPath}\n`);
|
|
416
429
|
if (!optionFlag(parsed, 'skip-mcp')) {
|
|
@@ -427,6 +440,7 @@ async function runInit(parsed) {
|
|
|
427
440
|
const defaults = await loadRuntimeDefaults(parsed);
|
|
428
441
|
const client = resolveSelectedClient(parsed, defaults);
|
|
429
442
|
const nextDefaults = deriveDefaultsForClient(parsed, defaults, client);
|
|
443
|
+
validateStoredToken(nextDefaults);
|
|
430
444
|
const configPath = await saveResolvedConfig(nextDefaults);
|
|
431
445
|
process.stdout.write(`Đã lưu config CLI tại ${configPath}\n`);
|
|
432
446
|
if (!optionFlag(parsed, 'skip-mcp') && client) {
|
|
@@ -457,7 +471,7 @@ async function runAdd(parsed) {
|
|
|
457
471
|
}
|
|
458
472
|
async function runTokenAdd(parsed) {
|
|
459
473
|
const defaults = await loadRuntimeDefaults(parsed);
|
|
460
|
-
const token = optionString(parsed, 'token') ?? optionString(parsed, 'mcp-token') ?? defaults.mcpToken;
|
|
474
|
+
const token = validateMcpToken(optionString(parsed, 'token') ?? optionString(parsed, 'mcp-token') ?? defaults.mcpToken);
|
|
461
475
|
if (!token?.trim()) {
|
|
462
476
|
throw new Error('Thiếu token. Dùng --token <token>.');
|
|
463
477
|
}
|
|
@@ -471,7 +485,7 @@ async function runTokenAdd(parsed) {
|
|
|
471
485
|
}
|
|
472
486
|
async function runTokenVerify(parsed) {
|
|
473
487
|
const defaults = await loadRuntimeDefaults(parsed);
|
|
474
|
-
const token = optionString(parsed, 'token') ?? optionString(parsed, 'mcp-token') ?? defaults.mcpToken;
|
|
488
|
+
const token = validateMcpToken(optionString(parsed, 'token') ?? optionString(parsed, 'mcp-token') ?? defaults.mcpToken);
|
|
475
489
|
if (!token?.trim()) {
|
|
476
490
|
throw new Error('Thiếu token. Dùng --token <token>.');
|
|
477
491
|
}
|
|
@@ -523,9 +537,9 @@ async function runExplain(parsed) {
|
|
|
523
537
|
if (skillClient) {
|
|
524
538
|
const skillTarget = resolveSkillInstallTarget({
|
|
525
539
|
client: skillClient,
|
|
526
|
-
scope,
|
|
540
|
+
scope: supportedScope(optionString(parsed, 'scope')),
|
|
527
541
|
cwd: optionString(parsed, 'cwd'),
|
|
528
|
-
targetDir: optionString(parsed, 'target-dir')
|
|
542
|
+
targetDir: optionString(parsed, 'target-dir'),
|
|
529
543
|
serverAlias: defaults.serverAlias,
|
|
530
544
|
mcpUrl: defaults.mcpUrl,
|
|
531
545
|
docsUrl: defaults.docsUrl,
|
package/dist/lib/doctor.js
CHANGED
|
@@ -7,6 +7,18 @@ function normalizeEndpoint(value) {
|
|
|
7
7
|
const trimmed = value.trim();
|
|
8
8
|
return trimmed ? trimmed.replace(/\/+$/, '') : undefined;
|
|
9
9
|
}
|
|
10
|
+
function parseStringHeaders(value) {
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const headers = {};
|
|
15
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
16
|
+
if (typeof headerValue === 'string') {
|
|
17
|
+
headers[key] = headerValue;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
21
|
+
}
|
|
10
22
|
function parseJsonEntries(raw, client) {
|
|
11
23
|
const parsed = JSON.parse(raw);
|
|
12
24
|
let sectionKey = 'mcpServers';
|
|
@@ -28,29 +40,16 @@ function parseJsonEntries(raw, client) {
|
|
|
28
40
|
return [];
|
|
29
41
|
}
|
|
30
42
|
const entry = value;
|
|
31
|
-
const headers = parseStringHeaders(entry.headers);
|
|
32
43
|
return [
|
|
33
44
|
{
|
|
34
45
|
alias,
|
|
35
46
|
endpoint: normalizeEndpoint(entry[endpointKey]),
|
|
36
|
-
headers,
|
|
47
|
+
headers: parseStringHeaders(entry.headers),
|
|
37
48
|
oauth: entry.oauth,
|
|
38
49
|
},
|
|
39
50
|
];
|
|
40
51
|
});
|
|
41
52
|
}
|
|
42
|
-
function parseStringHeaders(value) {
|
|
43
|
-
if (!value || typeof value !== 'object') {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
const headers = {};
|
|
47
|
-
for (const [key, headerValue] of Object.entries(value)) {
|
|
48
|
-
if (typeof headerValue === 'string') {
|
|
49
|
-
headers[key] = headerValue;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
53
|
-
}
|
|
54
53
|
function parseTomlEntries(raw) {
|
|
55
54
|
const entries = new Map();
|
|
56
55
|
let currentAlias;
|
|
@@ -141,14 +140,41 @@ function analyzeAuthorization(headerValue) {
|
|
|
141
140
|
}
|
|
142
141
|
const envVar = match[1];
|
|
143
142
|
const envValue = process.env[envVar]?.trim();
|
|
143
|
+
if (!envValue) {
|
|
144
|
+
return {
|
|
145
|
+
authorizationState: 'env-missing',
|
|
146
|
+
authorizationSource: 'env',
|
|
147
|
+
authorizationEnvVar: envVar,
|
|
148
|
+
likelyOAuthLogin: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (envValue.startsWith('pat_')) {
|
|
152
|
+
return {
|
|
153
|
+
authorizationState: 'invalid',
|
|
154
|
+
authorizationSource: 'env',
|
|
155
|
+
authorizationEnvVar: envVar,
|
|
156
|
+
authorizationInvalidReason: 'pat_token',
|
|
157
|
+
likelyOAuthLogin: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
144
160
|
return {
|
|
145
|
-
authorizationState:
|
|
161
|
+
authorizationState: 'env-present',
|
|
146
162
|
authorizationSource: 'env',
|
|
147
163
|
authorizationEnvVar: envVar,
|
|
148
|
-
likelyOAuthLogin:
|
|
164
|
+
likelyOAuthLogin: false,
|
|
149
165
|
};
|
|
150
166
|
}
|
|
151
|
-
|
|
167
|
+
const bearerMatch = trimmed.match(/^Bearer\s+(\S+)$/i);
|
|
168
|
+
if (bearerMatch) {
|
|
169
|
+
const token = bearerMatch[1];
|
|
170
|
+
if (token.startsWith('pat_')) {
|
|
171
|
+
return {
|
|
172
|
+
authorizationState: 'invalid',
|
|
173
|
+
authorizationSource: 'literal',
|
|
174
|
+
authorizationInvalidReason: 'pat_token',
|
|
175
|
+
likelyOAuthLogin: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
152
178
|
return {
|
|
153
179
|
authorizationState: 'present',
|
|
154
180
|
authorizationSource: 'literal',
|
|
@@ -216,12 +242,19 @@ export async function diagnoseMcpConfig(options) {
|
|
|
216
242
|
issues.push(`Authorization đang dùng biến môi trường ${authorization.authorizationEnvVar} nhưng biến này chưa có giá trị trong phiên hiện tại.`);
|
|
217
243
|
}
|
|
218
244
|
else if (authorization.authorizationState === 'invalid') {
|
|
219
|
-
|
|
245
|
+
if (authorization.authorizationInvalidReason === 'pat_token') {
|
|
246
|
+
issues.push('Authorization đang dùng token dạng pat_. Đây có vẻ là token_id hoặc mã hiển thị, không phải generated_token bí mật cho MCP. Hãy dùng token bắt đầu bằng grc_pat_.');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
issues.push('Authorization header có định dạng lạ, IDE có thể bỏ qua và bật OAuth.');
|
|
250
|
+
}
|
|
220
251
|
}
|
|
221
252
|
const oauthDisabled = options.client === 'opencode' && typeof entry.oauth === 'boolean'
|
|
222
253
|
? entry.oauth === false
|
|
223
254
|
: undefined;
|
|
224
|
-
if (options.client === 'opencode' &&
|
|
255
|
+
if (options.client === 'opencode' &&
|
|
256
|
+
authorization.authorizationState !== 'missing' &&
|
|
257
|
+
oauthDisabled !== true) {
|
|
225
258
|
issues.push('OpenCode chưa có oauth=false dù đã cấu hình Authorization header.');
|
|
226
259
|
}
|
|
227
260
|
return {
|
|
@@ -241,13 +274,13 @@ export function formatMcpDoctorResult(result) {
|
|
|
241
274
|
`MCP config cho ${result.client}: ${result.path} (${result.scope})`,
|
|
242
275
|
];
|
|
243
276
|
if (!result.exists) {
|
|
244
|
-
lines.push(
|
|
277
|
+
lines.push('- Trạng thái file: thiếu hoặc không đọc được');
|
|
245
278
|
for (const issue of result.issues) {
|
|
246
279
|
lines.push(`- Cảnh báo: ${issue}`);
|
|
247
280
|
}
|
|
248
281
|
return lines;
|
|
249
282
|
}
|
|
250
|
-
lines.push(
|
|
283
|
+
lines.push('- Trạng thái file: đã đọc được');
|
|
251
284
|
lines.push(`- Server entry: ${result.entryFound
|
|
252
285
|
? `${result.matchedAlias} (${result.matchedBy === 'endpoint' ? 'khớp theo endpoint' : 'khớp theo alias'})`
|
|
253
286
|
: 'không tìm thấy'}`);
|
|
@@ -261,7 +294,9 @@ export function formatMcpDoctorResult(result) {
|
|
|
261
294
|
: result.authorizationState === 'env-missing'
|
|
262
295
|
? `dùng biến môi trường ${result.authorizationEnvVar} nhưng hiện chưa có giá trị`
|
|
263
296
|
: result.authorizationState === 'invalid'
|
|
264
|
-
?
|
|
297
|
+
? result.authorizationInvalidReason === 'pat_token'
|
|
298
|
+
? 'token hiện tại là pat_ nên không hợp lệ cho MCP'
|
|
299
|
+
: 'có header nhưng định dạng không hợp lệ'
|
|
265
300
|
: 'thiếu hoàn toàn';
|
|
266
301
|
lines.push(`- Authorization: ${authText}`);
|
|
267
302
|
if (typeof result.oauthDisabled === 'boolean') {
|
package/dist/lib/skill.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
const seoBoosterRemoteUrl = 'https://gitlab.com/secomapp/development/seo-booster.git';
|
|
5
|
+
const seoBoosterProjectGuard = [
|
|
6
|
+
'- SP-RAG chỉ dành cho repo seo-booster.',
|
|
7
|
+
'- Trước tiên, nếu basename là `seo-booster` hoặc `sb` thì được phép gọi SP-RAG ngay.',
|
|
8
|
+
`- Nếu basename không phải \`seo-booster\` hoặc \`sb\`, hãy kiểm tra \`git remote get-url origin\` và chỉ gọi SP-RAG khi remote hiện tại là \`${seoBoosterRemoteUrl}\`.`,
|
|
9
|
+
'- Nếu project hiện tại không khớp hai điều kiện trên, không được gọi SP-RAG và phải nói rõ MCP này chỉ dành cho seo-booster.',
|
|
10
|
+
].join('\n');
|
|
4
11
|
function clientLabel(client) {
|
|
5
12
|
switch (client) {
|
|
6
13
|
case 'codex':
|
|
@@ -22,6 +29,7 @@ function normalizeScope(client, scope) {
|
|
|
22
29
|
return scope;
|
|
23
30
|
}
|
|
24
31
|
switch (client) {
|
|
32
|
+
case 'claude-code':
|
|
25
33
|
case 'cursor':
|
|
26
34
|
return 'project';
|
|
27
35
|
case 'vscode':
|
|
@@ -43,7 +51,9 @@ export function defaultSkillDir(client = 'codex', cwd, scope) {
|
|
|
43
51
|
case 'codex':
|
|
44
52
|
return path.join(os.homedir(), '.codex', 'skills', 'sp-rag');
|
|
45
53
|
case 'claude-code':
|
|
46
|
-
return
|
|
54
|
+
return normalizedScope === 'project'
|
|
55
|
+
? path.join(requireProjectCwd(client, cwd), '.claude', 'skills', 'sp-rag')
|
|
56
|
+
: path.join(os.homedir(), '.claude', 'skills', 'sp-rag');
|
|
47
57
|
case 'antigravity':
|
|
48
58
|
return path.join(os.homedir(), '.gemini', 'antigravity', 'skills', 'sp-rag');
|
|
49
59
|
case 'opencode':
|
|
@@ -89,6 +99,7 @@ Docs URL: \`${context.docsUrl}\`
|
|
|
89
99
|
|
|
90
100
|
## Guardrails
|
|
91
101
|
|
|
102
|
+
${seoBoosterProjectGuard}
|
|
92
103
|
- You must call SP-RAG MCP tools first for codebase or domain questions before using local workspace search, grep, or file reads.
|
|
93
104
|
- Prefer MCP-grounded answers before relying on memory.
|
|
94
105
|
- Treat \`answer_brief\` as a hint only. Prefer the richer evidence in \`matched_passages\`, \`top_entities\`, \`top_relations\`, and \`citations\` when writing the final answer.
|
|
@@ -107,6 +118,7 @@ alwaysApply: true
|
|
|
107
118
|
|
|
108
119
|
# SP-RAG
|
|
109
120
|
|
|
121
|
+
${seoBoosterProjectGuard}
|
|
110
122
|
- You must call the \`${context.serverAlias}\` MCP server first before using local workspace search, grep, or file reads for codebase and domain questions.
|
|
111
123
|
- Use rendered docs from \`${context.docsUrl}\` when documentation already answers the question.
|
|
112
124
|
- For architecture, domain, entities, relations, and business workflow questions, query MCP first and only then synthesize the answer.
|
|
@@ -141,6 +153,7 @@ You must call an SP-RAG MCP tool before using local workspace search, grep, or f
|
|
|
141
153
|
|
|
142
154
|
## Guardrails
|
|
143
155
|
|
|
156
|
+
${seoBoosterProjectGuard}
|
|
144
157
|
- Prefer MCP-grounded evidence before answering from memory.
|
|
145
158
|
- Treat \`answer_brief\` as a hint. Prefer the richer evidence in \`matched_passages\`, \`top_entities\`, \`top_relations\`, and \`citations\` when writing the final answer.
|
|
146
159
|
- Do not use local workspace search, grep, or file reads until SP-RAG MCP has been tried first for the current question.
|
|
@@ -153,6 +166,7 @@ You must call an SP-RAG MCP tool before using local workspace search, grep, or f
|
|
|
153
166
|
function renderVsCodeAlwaysOnInstructions(context) {
|
|
154
167
|
return `# SP-RAG workspace instructions
|
|
155
168
|
|
|
169
|
+
${seoBoosterProjectGuard}
|
|
156
170
|
- Always use the \`${context.serverAlias}\` MCP server first for codebase, domain, architecture, workflow, and operational questions before local workspace search, grep, or file reads.
|
|
157
171
|
- Start with \`healthz\` only when connectivity is uncertain or the session is new.
|
|
158
172
|
- Use \`query_context\` first for feature, entity, relation, and business-flow questions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp-rag",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "CLI cho setup MCP, codegraph GitNexus và skill của SP-RAG",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"sp-rag": "dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsc -p tsconfig.json",
|
|
14
|
-
"prepack": "npm run build",
|
|
15
|
-
"start": "node dist/index.js",
|
|
16
|
-
"test": "vitest run"
|
|
17
|
-
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"prepack": "npm run build",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=20"
|
|
20
20
|
},
|