sp-rag 0.6.0 → 0.6.2

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 CHANGED
@@ -17,7 +17,7 @@ CLI để setup nhanh SP-RAG theo hướng dev-friendly:
17
17
  ## Trạng thái package
18
18
 
19
19
  - package npm public: `sp-rag`
20
- - version đang publish: `0.6.0`
20
+ - version đang publish: `0.6.2`
21
21
  - binary public: `sp-rag`
22
22
 
23
23
  ## Cài từ source trong monorepo
@@ -36,22 +36,47 @@ node dist/index.js doctor
36
36
  npx sp-rag@latest install --client codex --mcp-token <grc_pat_...> --doctor
37
37
  npx sp-rag@latest install --client cursor --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
38
38
  npx sp-rag@latest install --client vscode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
39
- npx sp-rag@latest token add --token <grc_pat_...> --client codex
39
+ npx sp-rag@latest add --client claude-code --scope project --cwd D:/Webs/seo-booster
40
+ npx sp-rag@latest explain --client vscode --scope project --cwd D:/Webs/seo-booster
41
+ npx sp-rag@latest token add --token <grc_pat_...>
40
42
  npx sp-rag@latest token verify --token <grc_pat_...>
41
- npx sp-rag@latest mcp add antigravity --mcp-token <grc_pat_...>
42
- npx sp-rag@latest mcp add opencode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
43
- npx sp-rag@latest skill install --skill-client cursor --scope project --cwd D:/Webs/seo-booster
44
- npx sp-rag@latest skill install --skill-client vscode --scope project --cwd D:/Webs/seo-booster
43
+ npx sp-rag@latest mcp add antigravity
44
+ npx sp-rag@latest mcp add opencode --scope project --cwd D:/Webs/seo-booster
45
+ npx sp-rag@latest skill install --client cursor --scope project --cwd D:/Webs/seo-booster
46
+ npx sp-rag@latest skill install --client vscode --scope project --cwd D:/Webs/seo-booster
45
47
  ```
46
48
 
47
49
  Tương đương bằng `npm`:
48
50
 
49
51
  ```bash
50
52
  npm exec --yes sp-rag@latest install -- --client codex --mcp-token <grc_pat_...> --doctor
51
- npm exec --yes sp-rag@latest token add -- --token <grc_pat_...> --client codex
53
+ npm exec --yes sp-rag@latest add -- --client claude-code --scope project --cwd D:/Webs/seo-booster
54
+ npm exec --yes sp-rag@latest explain -- --client vscode --scope project --cwd D:/Webs/seo-booster
55
+ npm exec --yes sp-rag@latest token add -- --token <grc_pat_...>
52
56
  npm exec --yes sp-rag@latest token verify -- --token <grc_pat_...>
53
57
  ```
54
58
 
59
+ ## Flow gọn cho dev
60
+
61
+ Flow khuyên dùng sau khi đã có `grc_pat_*`:
62
+
63
+ 1. chạy `install` đúng một lần cho client đầu tiên, có kèm `--mcp-token`
64
+ 2. từ lần sau, dùng `add --client ...` để cài thêm MCP + skill cho client khác mà không phải nhập lại token
65
+ 3. khi chỉ muốn làm một nửa, dùng `mcp add` hoặc `skill install`
66
+ 4. khi đổi token, chỉ cần `token add`
67
+ 5. khi muốn kiểm tra máy đang được cấu hình ra sao, dùng `explain`
68
+
69
+ Ví dụ:
70
+
71
+ ```bash
72
+ sp-rag install --client vscode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...> --doctor
73
+ sp-rag add --client cursor --scope project --cwd D:/Webs/seo-booster
74
+ sp-rag mcp add antigravity
75
+ sp-rag skill install --client vscode --scope project --cwd D:/Webs/seo-booster
76
+ sp-rag token add --token <grc_pat_moi>
77
+ sp-rag explain --client vscode --scope project --cwd D:/Webs/seo-booster
78
+ ```
79
+
55
80
  ## Lấy token ở đâu
56
81
 
57
82
  `sp-rag` public bây giờ dùng token gắn với tài khoản thật trên server.
@@ -69,6 +94,7 @@ Luồng chuẩn:
69
94
  - quyền nhìn thấy tool/resource trên MCP bám theo role của account đó
70
95
  - `viewer`, `operator`, `approver`, `admin` sẽ thấy inventory khác nhau
71
96
  - `MCP_SERVER_ACCESS_TOKENS_JSON` chỉ còn là fallback legacy/break-glass, không còn là luồng chính cho dev
97
+ - khi client đã có sẵn một alias trỏ cùng MCP endpoint, CLI sẽ ưu tiên cập nhật alias đó thay vì tạo alias mới song song
72
98
 
73
99
  Tự tạo PAT bằng API hiện có:
74
100
 
@@ -100,6 +126,7 @@ Ví dụ dev dùng token trực tiếp:
100
126
  ```bash
101
127
  npx sp-rag@latest install --client cursor --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
102
128
  npx sp-rag@latest token verify --token <grc_pat_...>
129
+ npx sp-rag@latest explain --client cursor --scope project --cwd D:/Webs/seo-booster
103
130
  ```
104
131
 
105
132
  Hoặc dùng biến môi trường:
@@ -163,7 +190,9 @@ Ghi chú:
163
190
 
164
191
  ```bash
165
192
  sp-rag install --client codex --mcp-token <grc_pat_...> --doctor
166
- sp-rag token add --token <grc_pat_...> --client codex
193
+ sp-rag add --client cursor --scope project --cwd D:/Webs/seo-booster
194
+ sp-rag explain --client codex
195
+ sp-rag token add --token <grc_pat_...>
167
196
  sp-rag token verify --token <grc_pat_...>
168
197
  sp-rag config show
169
198
  sp-rag codegraph status
@@ -171,12 +200,12 @@ sp-rag codegraph watch --interval-ms 2000
171
200
  sp-rag codegraph runs --limit 5
172
201
  sp-rag codegraph metrics
173
202
  sp-rag codegraph recover --reason "Ops dọn stale run sau crash"
174
- sp-rag mcp add antigravity --mcp-token <grc_pat_...>
175
- sp-rag mcp add vscode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
176
- sp-rag mcp add opencode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
177
- sp-rag skill install
178
- sp-rag skill install --skill-client cursor --scope project --cwd D:/Webs/seo-booster
179
- sp-rag skill install --skill-client vscode --scope project --cwd D:/Webs/seo-booster
203
+ sp-rag mcp add antigravity
204
+ sp-rag mcp add vscode --scope project --cwd D:/Webs/seo-booster
205
+ sp-rag mcp add opencode --scope project --cwd D:/Webs/seo-booster
206
+ sp-rag skill install --client codex
207
+ sp-rag skill install --client cursor --scope project --cwd D:/Webs/seo-booster
208
+ sp-rag skill install --client vscode --scope project --cwd D:/Webs/seo-booster
180
209
  sp-rag eval run --file ./examples/eval-suite.sample.json
181
210
  ```
182
211
 
@@ -184,7 +213,9 @@ sp-rag eval run --file ./examples/eval-suite.sample.json
184
213
 
185
214
  ```bash
186
215
  sp-rag install --client codex --mcp-token <grc_pat_...> --doctor
187
- sp-rag token add --token <grc_pat_...> --client codex
216
+ sp-rag add --client cursor --scope project --cwd D:/Webs/seo-booster
217
+ sp-rag explain --client codex
218
+ sp-rag token add --token <grc_pat_...>
188
219
  sp-rag token verify --token <grc_pat_...>
189
220
  sp-rag config show
190
221
  sp-rag doctor
@@ -195,13 +226,13 @@ sp-rag codegraph metrics
195
226
  sp-rag codegraph recover --reason "Ops dọn stale run sau crash"
196
227
  sp-rag codegraph sync --branch master --commit-sha <sha> --webhook-token <token webhook codegraph> --gitlab-job-token <ci-job-token>
197
228
  sp-rag docs get public --format md
198
- sp-rag mcp add codex --mcp-token <grc_pat_...>
199
- sp-rag mcp add antigravity --mcp-token <grc_pat_...>
200
- sp-rag mcp add vscode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
201
- sp-rag mcp add opencode --scope project --cwd D:/Webs/seo-booster --mcp-token <grc_pat_...>
202
- sp-rag skill install
203
- sp-rag skill install --skill-client cursor --scope project --cwd D:/Webs/seo-booster
204
- sp-rag skill install --skill-client vscode --scope project --cwd D:/Webs/seo-booster
229
+ sp-rag mcp add codex
230
+ sp-rag mcp add antigravity
231
+ sp-rag mcp add vscode --scope project --cwd D:/Webs/seo-booster
232
+ sp-rag mcp add opencode --scope project --cwd D:/Webs/seo-booster
233
+ sp-rag skill install --client codex
234
+ sp-rag skill install --client cursor --scope project --cwd D:/Webs/seo-booster
235
+ sp-rag skill install --client vscode --scope project --cwd D:/Webs/seo-booster
205
236
  sp-rag eval run --file ./examples/eval-suite.sample.json
206
237
  sp-rag update setup --client codex
207
238
  ```
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { loadCliConfig, saveCliConfig, } from './lib/config-store.js';
3
3
  import { runEvaluationSuite } from './lib/eval.js';
4
- import { defaultBaseUrl, defaultMcpServerAlias, defaultMcpUrl, installMcpConfig, } from './lib/mcp-config.js';
4
+ import { defaultBaseUrl, defaultMcpServerAlias, defaultMcpUrl, installMcpConfig, resolveMcpConfigPath, } from './lib/mcp-config.js';
5
5
  import { fetchJson, fetchText, runDoctor } from './lib/http.js';
6
- import { installSkill, } from './lib/skill.js';
6
+ import { installSkill, resolveSkillInstallTarget, } from './lib/skill.js';
7
7
  function parseArgv(argv) {
8
8
  const positionals = [];
9
9
  const options = {};
@@ -111,8 +111,11 @@ async function loadRuntimeDefaults(parsed) {
111
111
  const docsUrl = optionString(parsed, 'docs-url') ??
112
112
  config?.docsUrl ??
113
113
  `${baseUrl.replace(/\/+$/, '')}/codegraph/docs/public?format=md`;
114
- const defaultClient = supportedClient(optionString(parsed, 'client') ?? config?.defaultClient);
114
+ const explicitClient = supportedClient(optionString(parsed, 'client'));
115
+ const defaultClient = explicitClient ?? supportedClient(config?.defaultClient);
115
116
  const defaultScope = supportedScope(optionString(parsed, 'scope') ?? config?.defaultScope);
117
+ const explicitSkillClient = supportedSkillClient(optionString(parsed, 'skill-client'));
118
+ const derivedSkillClient = defaultSkillClientForMcpClient(defaultClient);
116
119
  return {
117
120
  config,
118
121
  homeDir,
@@ -127,21 +130,58 @@ async function loadRuntimeDefaults(parsed) {
127
130
  optionString(parsed, 'token') ??
128
131
  process.env['SP_RAG_MCP_TOKEN']?.trim() ??
129
132
  config?.mcpToken,
130
- skillClient: supportedSkillClient(optionString(parsed, 'skill-client')) ??
133
+ skillClient: explicitSkillClient ??
134
+ (explicitClient ? defaultSkillClientForMcpClient(explicitClient) : undefined) ??
131
135
  config?.skillClient ??
132
- defaultSkillClientForMcpClient(defaultClient),
136
+ derivedSkillClient,
133
137
  skillTargetDir: optionString(parsed, 'target-dir') ?? config?.skillTargetDir,
134
138
  };
135
139
  }
140
+ function resolveSelectedClient(parsed, defaults, explicitClient) {
141
+ return supportedClient(explicitClient ??
142
+ optionString(parsed, 'client') ??
143
+ parsed.positionals[2] ??
144
+ defaults.defaultClient);
145
+ }
146
+ function resolveSelectedSkillClient(parsed, defaults, client) {
147
+ return (supportedSkillClient(optionString(parsed, 'client')) ??
148
+ supportedSkillClient(optionString(parsed, 'skill-client')) ??
149
+ (client ? defaultSkillClientForMcpClient(client) : undefined) ??
150
+ defaults.skillClient);
151
+ }
152
+ function deriveDefaultsForClient(parsed, defaults, client) {
153
+ const nextClient = client ?? defaults.defaultClient;
154
+ const nextScope = supportedScope(optionString(parsed, 'scope')) ?? defaults.defaultScope;
155
+ const nextSkillClient = resolveSelectedSkillClient(parsed, defaults, nextClient);
156
+ return {
157
+ ...defaults,
158
+ defaultClient: nextClient,
159
+ defaultScope: nextScope,
160
+ skillClient: nextSkillClient,
161
+ };
162
+ }
163
+ function resolveAuthForMcp(parsed, defaults) {
164
+ const authToken = optionString(parsed, 'mcp-token') ?? defaults.mcpToken;
165
+ const authEnvVar = optionString(parsed, 'auth-env-var') ?? defaults.authEnvVar;
166
+ if (!authToken?.trim() && !authEnvVar?.trim()) {
167
+ 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>.');
168
+ }
169
+ return {
170
+ authToken: authToken?.trim(),
171
+ authEnvVar: authEnvVar?.trim(),
172
+ };
173
+ }
136
174
  function helpText() {
137
175
  return `sp-rag - CLI cho setup, MCP, codegraph, eval và skill của SP-RAG
138
176
 
139
177
  Lệnh chính:
140
- sp-rag install [--base-url URL] [--mcp-url URL] [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--skill-client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--mcp-token TOKEN] [--auth-env-var ENV_VAR] [--target-dir PATH]
178
+ sp-rag install --client codex|cursor|claude-code|antigravity|vscode|opencode [--base-url URL] [--mcp-url URL] [--scope global|project] [--mcp-token TOKEN] [--auth-env-var ENV_VAR] [--target-dir PATH] [--doctor]
179
+ sp-rag add --client codex|cursor|claude-code|antigravity|vscode|opencode [--scope global|project] [--cwd PATH] [--target-dir PATH]
141
180
  sp-rag init [--base-url URL] [--mcp-url URL] [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--skill-client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--auth-env-var ENV_VAR] [--target-dir PATH]
142
- sp-rag token add --token TOKEN [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--cwd PATH]
181
+ sp-rag token add --token TOKEN
143
182
  sp-rag token verify [--token TOKEN] [--base-url URL]
144
183
  sp-rag config show
184
+ sp-rag explain [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--cwd PATH]
145
185
  sp-rag doctor [--base-url URL]
146
186
  sp-rag codegraph status [--base-url URL]
147
187
  sp-rag codegraph watch [--base-url URL] [--interval-ms N] [--max-polls N]
@@ -151,7 +191,7 @@ Lệnh chính:
151
191
  sp-rag codegraph sync [--base-url URL] [--branch BRANCH] [--commit-sha SHA] [--force] [--webhook-token TOKEN] [--gitlab-job-token TOKEN]
152
192
  sp-rag docs get <public|function|dev> [--base-url URL] [--format md|json|html]
153
193
  sp-rag mcp add <codex|cursor|claude-code|antigravity|vscode|opencode> [--url URL] [--scope global|project] [--auth-env-var ENV_VAR] [--mcp-token TOKEN]
154
- sp-rag skill install [--skill-client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--cwd PATH] [--target-dir PATH] [--mcp-url URL] [--docs-url URL]
194
+ sp-rag skill install [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--skill-client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--cwd PATH] [--target-dir PATH] [--mcp-url URL] [--docs-url URL]
155
195
  sp-rag eval run --file eval-suite.json [--base-url URL]
156
196
  sp-rag update setup [--client codex|cursor|claude-code|antigravity|vscode|opencode] [--skill-client codex|cursor|claude-code|antigravity|vscode|opencode] [--scope global|project] [--url URL] [--auth-env-var ENV_VAR] [--target-dir PATH]
157
197
  `;
@@ -296,7 +336,7 @@ async function runDocsGet(parsed) {
296
336
  process.stdout.write(`${result}${result.endsWith('\n') ? '' : '\n'}`);
297
337
  }
298
338
  async function runMcpAdd(parsed, defaults, explicitClient) {
299
- const client = supportedClient(explicitClient ?? parsed.positionals[2] ?? defaults.defaultClient);
339
+ const client = resolveSelectedClient(parsed, defaults, explicitClient);
300
340
  if (!client) {
301
341
  throw new Error('Thiếu client. Dùng codex, cursor, claude-code, antigravity, vscode hoặc opencode.');
302
342
  }
@@ -304,17 +344,15 @@ async function runMcpAdd(parsed, defaults, explicitClient) {
304
344
  client,
305
345
  url: optionString(parsed, 'url') ?? defaults.mcpUrl,
306
346
  scope: supportedScope(optionString(parsed, 'scope')) ?? defaults.defaultScope,
307
- authEnvVar: optionString(parsed, 'auth-env-var') ?? defaults.authEnvVar,
308
- authToken: optionString(parsed, 'mcp-token') ?? defaults.mcpToken,
347
+ authEnvVar: resolveAuthForMcp(parsed, defaults).authEnvVar,
348
+ authToken: resolveAuthForMcp(parsed, defaults).authToken,
309
349
  cwd: optionString(parsed, 'cwd'),
310
350
  serverAlias: optionString(parsed, 'server-alias') ?? defaults.serverAlias,
311
351
  });
312
352
  process.stdout.write(`Đã cập nhật cấu hình MCP cho ${result.client} tại ${result.path} (${result.scope}).\n`);
313
353
  }
314
- async function runSkillInstall(parsed, defaults) {
315
- const client = supportedSkillClient(optionString(parsed, 'skill-client')) ??
316
- defaults.skillClient ??
317
- 'codex';
354
+ async function runSkillInstall(parsed, defaults, explicitClient) {
355
+ const client = resolveSelectedSkillClient(parsed, defaults, explicitClient) ?? 'codex';
318
356
  const result = await installSkill({
319
357
  client,
320
358
  cwd: optionString(parsed, 'cwd'),
@@ -326,26 +364,60 @@ async function runSkillInstall(parsed, defaults) {
326
364
  });
327
365
  process.stdout.write(`Đã cài skill cho ${result.client} tại ${result.path}\n`);
328
366
  }
367
+ async function saveResolvedConfig(defaults) {
368
+ return saveCliConfig(buildCliConfig(defaults), defaults.homeDir);
369
+ }
370
+ async function runClientSetup(parsed, defaults, client) {
371
+ const nextDefaults = deriveDefaultsForClient(parsed, defaults, client);
372
+ const configPath = await saveResolvedConfig(nextDefaults);
373
+ process.stdout.write(`Đã lưu config CLI tại ${configPath}\n`);
374
+ if (!optionFlag(parsed, 'skip-mcp')) {
375
+ await runMcpAdd(parsed, nextDefaults, client);
376
+ }
377
+ if (!optionFlag(parsed, 'skip-skill')) {
378
+ await runSkillInstall(parsed, nextDefaults, client);
379
+ }
380
+ if (optionFlag(parsed, 'doctor')) {
381
+ const results = await runDoctor({ baseUrl: nextDefaults.baseUrl });
382
+ for (const result of results) {
383
+ process.stdout.write(`${result.ok ? 'OK' : 'FAIL'} ${result.name} ${result.status} ${result.url}\n`);
384
+ }
385
+ }
386
+ }
329
387
  async function runInit(parsed) {
330
388
  const defaults = await loadRuntimeDefaults(parsed);
331
- const config = buildCliConfig(defaults);
332
- const configPath = await saveCliConfig(config, defaults.homeDir);
389
+ const client = resolveSelectedClient(parsed, defaults);
390
+ const nextDefaults = deriveDefaultsForClient(parsed, defaults, client);
391
+ const configPath = await saveResolvedConfig(nextDefaults);
333
392
  process.stdout.write(`Đã lưu config CLI tại ${configPath}\n`);
334
- if (!optionFlag(parsed, 'skip-mcp') && defaults.defaultClient) {
335
- await runMcpAdd(parsed, defaults, defaults.defaultClient);
393
+ if (!optionFlag(parsed, 'skip-mcp') && client) {
394
+ await runMcpAdd(parsed, nextDefaults, client);
336
395
  }
337
- if (!optionFlag(parsed, 'skip-skill') && defaults.skillClient) {
338
- await runSkillInstall(parsed, defaults);
396
+ if (!optionFlag(parsed, 'skip-skill') && nextDefaults.skillClient) {
397
+ await runSkillInstall(parsed, nextDefaults, client);
339
398
  }
340
399
  if (optionFlag(parsed, 'doctor')) {
341
- const results = await runDoctor({ baseUrl: defaults.baseUrl });
400
+ const results = await runDoctor({ baseUrl: nextDefaults.baseUrl });
342
401
  for (const result of results) {
343
402
  process.stdout.write(`${result.ok ? 'OK' : 'FAIL'} ${result.name} ${result.status} ${result.url}\n`);
344
403
  }
345
404
  }
346
405
  }
347
406
  async function runInstall(parsed) {
348
- await runInit(parsed);
407
+ const defaults = await loadRuntimeDefaults(parsed);
408
+ const client = resolveSelectedClient(parsed, defaults);
409
+ if (!client) {
410
+ throw new Error('Thiếu client. Dùng sp-rag install --client <client> --mcp-token <token>.');
411
+ }
412
+ await runClientSetup(parsed, defaults, client);
413
+ }
414
+ async function runAdd(parsed) {
415
+ const defaults = await loadRuntimeDefaults(parsed);
416
+ const client = resolveSelectedClient(parsed, defaults);
417
+ if (!client) {
418
+ throw new Error('Thiếu client. Dùng sp-rag add --client <client>.');
419
+ }
420
+ await runClientSetup(parsed, defaults, client);
349
421
  }
350
422
  async function runTokenAdd(parsed) {
351
423
  const defaults = await loadRuntimeDefaults(parsed);
@@ -357,24 +429,9 @@ async function runTokenAdd(parsed) {
357
429
  ...defaults,
358
430
  mcpToken: token.trim(),
359
431
  };
360
- const configPath = await saveCliConfig(buildCliConfig(nextDefaults), defaults.homeDir);
432
+ const configPath = await saveResolvedConfig(nextDefaults);
361
433
  process.stdout.write(`Đã lưu token MCP tại ${configPath}\n`);
362
- if (optionFlag(parsed, 'skip-mcp-update')) {
363
- return;
364
- }
365
- const client = supportedClient(optionString(parsed, 'client') ?? defaults.defaultClient);
366
- if (!client) {
367
- process.stdout.write('Chưa có client mặc định để cập nhật MCP config. Dùng thêm --client nếu muốn ghi ngay.\n');
368
- return;
369
- }
370
- await runMcpAdd({
371
- ...parsed,
372
- positionals: ['mcp', 'add', client],
373
- options: {
374
- ...parsed.options,
375
- 'mcp-token': token.trim(),
376
- },
377
- }, nextDefaults, client);
434
+ return;
378
435
  }
379
436
  async function runTokenVerify(parsed) {
380
437
  const defaults = await loadRuntimeDefaults(parsed);
@@ -395,6 +452,73 @@ async function runConfigShow(parsed) {
395
452
  const config = await loadCliConfig(optionString(parsed, 'home-dir'));
396
453
  process.stdout.write(`${JSON.stringify(config ?? {}, null, 2)}\n`);
397
454
  }
455
+ async function runExplain(parsed) {
456
+ const defaults = await loadRuntimeDefaults(parsed);
457
+ const client = resolveSelectedClient(parsed, defaults);
458
+ const scope = supportedScope(optionString(parsed, 'scope')) ?? defaults.defaultScope;
459
+ const tokenStatus = defaults.mcpToken?.trim()
460
+ ? 'đã lưu'
461
+ : defaults.authEnvVar?.trim()
462
+ ? `dùng biến môi trường ${defaults.authEnvVar}`
463
+ : 'chưa có';
464
+ const lines = [];
465
+ lines.push('## Cấu hình hiện tại');
466
+ lines.push(`- Base URL: ${defaults.baseUrl}`);
467
+ lines.push(`- MCP URL: ${defaults.mcpUrl}`);
468
+ lines.push(`- Client mặc định: ${defaults.defaultClient ?? 'chưa có'}`);
469
+ lines.push(`- Scope mặc định: ${defaults.defaultScope ?? 'chưa có'}`);
470
+ lines.push(`- Token MCP đang ở trạng thái: ${tokenStatus}`);
471
+ if (client) {
472
+ try {
473
+ const mcpTarget = resolveMcpConfigPath({
474
+ client,
475
+ url: defaults.mcpUrl,
476
+ scope,
477
+ cwd: optionString(parsed, 'cwd'),
478
+ });
479
+ lines.push(`- File MCP cho ${client}: ${mcpTarget.path}`);
480
+ }
481
+ catch (error) {
482
+ lines.push(`- File MCP cho ${client}: không xác định được (${error instanceof Error ? error.message : String(error)})`);
483
+ }
484
+ try {
485
+ const skillClient = resolveSelectedSkillClient(parsed, defaults, client) ??
486
+ defaultSkillClientForMcpClient(client);
487
+ if (skillClient) {
488
+ const skillTarget = resolveSkillInstallTarget({
489
+ client: skillClient,
490
+ scope,
491
+ cwd: optionString(parsed, 'cwd'),
492
+ targetDir: optionString(parsed, 'target-dir') ?? defaults.skillTargetDir,
493
+ serverAlias: defaults.serverAlias,
494
+ mcpUrl: defaults.mcpUrl,
495
+ docsUrl: defaults.docsUrl,
496
+ });
497
+ lines.push(`- File skill cho ${client}: ${skillTarget.path}`);
498
+ }
499
+ }
500
+ catch (error) {
501
+ lines.push(`- File skill cho ${client}: không xác định được (${error instanceof Error ? error.message : String(error)})`);
502
+ }
503
+ }
504
+ lines.push('');
505
+ lines.push('## Cách dùng chuẩn');
506
+ if (!defaults.mcpToken?.trim() && !defaults.authEnvVar?.trim()) {
507
+ lines.push('- Máy này chưa có token MCP. Hãy chạy:');
508
+ lines.push(` sp-rag install --client ${client ?? 'vscode'} --mcp-token <token>`);
509
+ }
510
+ else {
511
+ const chosenClient = client ?? defaults.defaultClient ?? 'vscode';
512
+ const chosenSkillClient = resolveSelectedSkillClient(parsed, defaults, client) ?? chosenClient;
513
+ lines.push('- Nếu muốn cài đầy đủ MCP + skill cho client mới, hãy chạy:');
514
+ lines.push(` sp-rag add --client ${chosenClient}`);
515
+ lines.push('- Nếu chỉ muốn cập nhật MCP, hãy chạy:');
516
+ lines.push(` sp-rag mcp add ${chosenClient}`);
517
+ lines.push('- Nếu chỉ muốn cài lại skill, hãy chạy:');
518
+ lines.push(` sp-rag skill install --client ${chosenSkillClient}`);
519
+ }
520
+ process.stdout.write(`${lines.join('\n')}\n`);
521
+ }
398
522
  async function runEval(parsed) {
399
523
  const filePath = optionString(parsed, 'file');
400
524
  if (!filePath) {
@@ -412,15 +536,15 @@ async function runEval(parsed) {
412
536
  }
413
537
  async function runUpdateSetup(parsed) {
414
538
  const defaults = await loadRuntimeDefaults(parsed);
415
- const client = optionString(parsed, 'client') ?? defaults.defaultClient;
539
+ const client = resolveSelectedClient(parsed, defaults);
416
540
  if (client) {
417
541
  await runMcpAdd({
418
542
  ...parsed,
419
543
  positionals: ['mcp', 'add', client],
420
544
  }, defaults, client);
421
545
  }
422
- if (!optionFlag(parsed, 'skip-skill') && defaults.skillClient) {
423
- await runSkillInstall(parsed, defaults);
546
+ if (!optionFlag(parsed, 'skip-skill') && resolveSelectedSkillClient(parsed, defaults, client)) {
547
+ await runSkillInstall(parsed, defaults, client);
424
548
  }
425
549
  }
426
550
  export async function runCli(argv) {
@@ -435,6 +559,10 @@ export async function runCli(argv) {
435
559
  await runInstall(parsed);
436
560
  return 0;
437
561
  }
562
+ if (group === 'add') {
563
+ await runAdd(parsed);
564
+ return 0;
565
+ }
438
566
  if (group === 'init') {
439
567
  await runInit(parsed);
440
568
  return 0;
@@ -451,6 +579,10 @@ export async function runCli(argv) {
451
579
  await runConfigShow(parsed);
452
580
  return 0;
453
581
  }
582
+ if (group === 'explain') {
583
+ await runExplain(parsed);
584
+ return 0;
585
+ }
454
586
  if (group === 'doctor') {
455
587
  const defaults = await loadRuntimeDefaults(parsed);
456
588
  const results = await runDoctor({
@@ -511,7 +643,7 @@ export async function runCli(argv) {
511
643
  return 0;
512
644
  }
513
645
  if (group === 'version') {
514
- process.stdout.write('sp-rag 0.6.0\n');
646
+ process.stdout.write('sp-rag 0.6.2\n');
515
647
  return 0;
516
648
  }
517
649
  process.stdout.write(helpText());
@@ -19,6 +19,13 @@ function withHeaders(target, headers) {
19
19
  headers,
20
20
  };
21
21
  }
22
+ function normalizeEndpoint(value) {
23
+ if (typeof value !== 'string') {
24
+ return null;
25
+ }
26
+ const trimmed = value.trim();
27
+ return trimmed ? trimmed.replace(/\/+$/, '') : null;
28
+ }
22
29
  function bearerHeader(authToken, authEnvVar, envStyle = 'shell') {
23
30
  const trimmedToken = authToken?.trim();
24
31
  if (trimmedToken) {
@@ -55,6 +62,29 @@ function upsertObjectEntry(base, sectionKey, entryKey, entryValue) {
55
62
  [sectionKey]: section,
56
63
  };
57
64
  }
65
+ function upsertObjectEntryPreservingAliasByEndpoint(base, sectionKey, entryKey, entryValue, endpointKey) {
66
+ const section = base[sectionKey] && typeof base[sectionKey] === 'object'
67
+ ? { ...base[sectionKey] }
68
+ : {};
69
+ const targetEndpoint = normalizeEndpoint(entryValue[endpointKey]);
70
+ const existingAlias = targetEndpoint
71
+ ? Object.entries(section).find(([alias, value]) => {
72
+ if (alias === entryKey || !value || typeof value !== 'object') {
73
+ return false;
74
+ }
75
+ return normalizeEndpoint(value[endpointKey]) === targetEndpoint;
76
+ })?.[0]
77
+ : undefined;
78
+ const aliasToWrite = existingAlias ?? entryKey;
79
+ section[aliasToWrite] = entryValue;
80
+ if (existingAlias && existingAlias !== entryKey && entryKey in section) {
81
+ delete section[entryKey];
82
+ }
83
+ return {
84
+ ...base,
85
+ [sectionKey]: section,
86
+ };
87
+ }
58
88
  function vscodeGlobalConfigPath(home) {
59
89
  if (process.platform === 'win32') {
60
90
  const appData = process.env['APPDATA']?.trim() || path.join(home, 'AppData', 'Roaming');
@@ -98,14 +128,14 @@ export function upsertJsonMcpConfig(existing, options) {
98
128
  ...(options.includeTypeHttp ? { type: 'http' } : {}),
99
129
  url: options.url,
100
130
  }, bearerHeader(options.authToken, options.authEnvVar, 'shell'));
101
- return `${JSON.stringify(upsertObjectEntry(base, 'mcpServers', options.serverAlias, serverConfig), null, 2)}\n`;
131
+ return `${JSON.stringify(upsertObjectEntryPreservingAliasByEndpoint(base, 'mcpServers', options.serverAlias, serverConfig, 'url'), null, 2)}\n`;
102
132
  }
103
133
  export function upsertAntigravityConfig(existing, options) {
104
134
  const base = parseJsonObject(existing);
105
135
  const serverConfig = withHeaders({
106
136
  serverUrl: options.url,
107
137
  }, bearerHeader(options.authToken, options.authEnvVar, 'shell'));
108
- return `${JSON.stringify(upsertObjectEntry(base, 'mcpServers', options.serverAlias, serverConfig), null, 2)}\n`;
138
+ return `${JSON.stringify(upsertObjectEntryPreservingAliasByEndpoint(base, 'mcpServers', options.serverAlias, serverConfig, 'serverUrl'), null, 2)}\n`;
109
139
  }
110
140
  export function upsertVsCodeConfig(existing, options) {
111
141
  const base = parseJsonObject(existing);
@@ -113,7 +143,7 @@ export function upsertVsCodeConfig(existing, options) {
113
143
  type: 'http',
114
144
  url: options.url,
115
145
  }, bearerHeader(options.authToken, options.authEnvVar, 'vscode'));
116
- return `${JSON.stringify(upsertObjectEntry(base, 'servers', options.serverAlias, serverConfig), null, 2)}\n`;
146
+ return `${JSON.stringify(upsertObjectEntryPreservingAliasByEndpoint(base, 'servers', options.serverAlias, serverConfig, 'url'), null, 2)}\n`;
117
147
  }
118
148
  export function upsertOpenCodeConfig(existing, options) {
119
149
  const base = parseJsonObject(existing);
@@ -130,7 +160,7 @@ export function upsertOpenCodeConfig(existing, options) {
130
160
  enabled: true,
131
161
  ...(headers ? { oauth: false } : {}),
132
162
  }, headers);
133
- return `${JSON.stringify(upsertObjectEntry(nextBase, 'mcp', options.serverAlias, serverConfig), null, 2)}\n`;
163
+ return `${JSON.stringify(upsertObjectEntryPreservingAliasByEndpoint(nextBase, 'mcp', options.serverAlias, serverConfig, 'url'), null, 2)}\n`;
134
164
  }
135
165
  export function resolveMcpConfigPath(options) {
136
166
  const inferredScope = options.client === 'codex' || options.client === 'antigravity'
package/dist/lib/skill.js CHANGED
@@ -156,6 +156,22 @@ function renderSkillArtifact(context) {
156
156
  };
157
157
  }
158
158
  }
159
+ export function resolveSkillInstallTarget(options = {}) {
160
+ const client = options.client ?? 'codex';
161
+ const scope = normalizeScope(client, options.scope);
162
+ const targetDir = path.resolve(options.targetDir ?? defaultSkillDir(client, options.cwd, scope));
163
+ const artifact = renderSkillArtifact({
164
+ client,
165
+ serverAlias: options.serverAlias?.trim() || 'sp-rag',
166
+ mcpUrl: options.mcpUrl?.trim() || 'https://sp-rag.secomapp.com/mcp',
167
+ docsUrl: options.docsUrl?.trim() || 'https://sp-rag.secomapp.com/codegraph/docs/public?format=md',
168
+ });
169
+ return {
170
+ client,
171
+ scope,
172
+ path: path.join(targetDir, artifact.fileName),
173
+ };
174
+ }
159
175
  export function renderSkill(options) {
160
176
  return renderSkillArtifact(options).content;
161
177
  }
@@ -166,21 +182,18 @@ export function renderCodexSkill(options) {
166
182
  });
167
183
  }
168
184
  export async function installSkill(options = {}) {
169
- const client = options.client ?? 'codex';
170
- const scope = normalizeScope(client, options.scope);
171
- const targetDir = path.resolve(options.targetDir ?? defaultSkillDir(client, options.cwd, scope));
185
+ const resolved = resolveSkillInstallTarget(options);
172
186
  const artifact = renderSkillArtifact({
173
- client,
187
+ client: resolved.client,
174
188
  serverAlias: options.serverAlias?.trim() || 'sp-rag',
175
189
  mcpUrl: options.mcpUrl?.trim() || 'https://sp-rag.secomapp.com/mcp',
176
190
  docsUrl: options.docsUrl?.trim() || 'https://sp-rag.secomapp.com/codegraph/docs/public?format=md',
177
191
  });
178
- const filePath = path.join(targetDir, artifact.fileName);
179
- await mkdir(targetDir, { recursive: true });
180
- await writeFile(filePath, artifact.content, 'utf8');
192
+ await mkdir(path.dirname(resolved.path), { recursive: true });
193
+ await writeFile(resolved.path, artifact.content, 'utf8');
181
194
  return {
182
- client,
183
- path: filePath,
195
+ client: resolved.client,
196
+ path: resolved.path,
184
197
  };
185
198
  }
186
199
  export async function installCodexSkill(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp-rag",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "CLI cho setup MCP, codegraph GitNexus và skill của SP-RAG",
5
5
  "type": "module",
6
6
  "files": [