@genrtl/grtl 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +27 -64
  2. package/dist/index.js +465 -233
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,21 +1,12 @@
1
- # grtl
1
+ # @genrtl/grtl
2
2
 
3
- Command-line client for the GenRTL RTL engineering knowledge service.
3
+ CLI and coding-agent integration for the GenRTL RTL engineering knowledge
4
+ service.
4
5
 
5
- ## Requirements
6
-
7
- - Node.js 20.12 or newer
8
- - A GenRTL API key
9
-
10
- ## Installation
6
+ ## Install
11
7
 
12
8
  ```bash
13
9
  npm install --global @genrtl/grtl
14
- ```
15
-
16
- Set the API key in your environment:
17
-
18
- ```bash
19
10
  export GRTL_API_KEY="gtr_live_your_api_key"
20
11
  ```
21
12
 
@@ -25,74 +16,48 @@ PowerShell:
25
16
  $env:GRTL_API_KEY = "gtr_live_your_api_key"
26
17
  ```
27
18
 
28
- `GENRTL_API_KEY` is also supported.
29
-
30
- ## Knowledge Tools
31
-
32
- The CLI exposes the same four tools as the hosted GenRTL MCP server:
33
-
34
- ```bash
35
- grtl genrtl_knowledge_search "How should this AXI stream handle backpressure?"
36
- grtl genrtl_spec2rtl_search "Implement an APB register block with byte enables"
37
- grtl genrtl_verification_search "Build a self-checking testbench for an async FIFO"
38
- grtl genrtl_debug_search "Explain this Vivado CDC warning and suggest a safe fix"
39
- ```
40
-
41
- Short aliases are available:
42
-
43
- ```bash
44
- grtl knowledge-search "..."
45
- grtl spec2rtl-search "..."
46
- grtl verification-search "..."
47
- grtl debug-search "..."
48
- ```
49
-
50
- Search options include `--type`, `--domain`, `--tool`, `--tool-version`,
51
- `--error-type`, `--severity`, `--interface`, `--target`, `--tag`, `--top-k`,
52
- `--min-score`, `--workspace-id`, and `--json`.
19
+ ## Agent Setup
53
20
 
54
- Example:
21
+ Install a Skill that tells the agent to call the `grtl` CLI:
55
22
 
56
23
  ```bash
57
- grtl debug-search "FSM hangs after reset" \
58
- --tool Vivado \
59
- --target fpga \
60
- --tag reset fsm \
61
- --top-k 8 \
62
- --json
24
+ grtl setup --cli --codex
25
+ grtl setup --cli --cursor --project
63
26
  ```
64
27
 
65
- ## Agent Setup
66
-
67
- Configure the hosted HTTP MCP endpoint for a supported coding agent:
28
+ Configure hosted MCP and install a Skill for the four MCP tools:
68
29
 
69
30
  ```bash
70
- grtl setup --cursor
71
- grtl setup --codex --project
72
- grtl setup --claude --opencode
31
+ grtl setup --mcp --codex
32
+ grtl setup --mcp --cursor --project
73
33
  ```
74
34
 
75
- Supported agent flags are `--claude`, `--cursor`, `--codex`, `--opencode`,
76
- `--gemini`, and `--antigravity`. Without a flag, `grtl setup` presents an
77
- interactive selection.
35
+ Without `--cli` or `--mcp`, setup asks which mode to install.
78
36
 
79
- The API key is written to the selected agent's MCP configuration because the
80
- agent must send it as a Bearer token. Protect that configuration file and do
81
- not commit project-level MCP configuration containing a real key.
37
+ For Codex, Skills are installed under `.agents/skills` for project setup or
38
+ `~/.agents/skills` for global setup. MCP mode also updates
39
+ `.codex/config.toml` or `~/.codex/config.toml`.
82
40
 
83
- The default endpoint is:
41
+ The hosted MCP endpoint is:
84
42
 
85
43
  ```text
86
- https://www.genrtl.com/api/mcp
44
+ https://genrtl.com/api/mcp
87
45
  ```
88
46
 
89
- Use `--base-url` for another deployment:
47
+ ## Knowledge Commands
90
48
 
91
49
  ```bash
92
- grtl --base-url http://localhost:3005 setup --cursor --project
93
- grtl --base-url http://localhost:3005 debug-search "compile error"
50
+ grtl knowledge-search "AXI stream backpressure design"
51
+ grtl spec2rtl-search "Design an APB register block"
52
+ grtl verification-search "Verify an async FIFO"
53
+ grtl debug-search "Explain this Vivado CDC warning"
94
54
  ```
95
55
 
56
+ Use `--json` for structured output. Available filters include `--type`,
57
+ `--domain`, `--tool`, `--tool-version`, `--error-type`, `--severity`,
58
+ `--interface`, `--target`, `--tag`, `--top-k`, `--min-score`, and
59
+ `--workspace-id`.
60
+
96
61
  ## Development
97
62
 
98
63
  ```bash
@@ -102,5 +67,3 @@ pnpm --filter @genrtl/grtl typecheck
102
67
  pnpm --filter @genrtl/grtl test
103
68
  pnpm --filter @genrtl/grtl build
104
69
  ```
105
-
106
- This project is derived from Upstash Context7 and retains its MIT license.
package/dist/index.js CHANGED
@@ -6,106 +6,16 @@ import pc6 from "picocolors";
6
6
  import figlet from "figlet";
7
7
 
8
8
  // src/commands/setup.ts
9
- import pc3 from "picocolors";
9
+ import { select } from "@inquirer/prompts";
10
+ import { dirname as dirname4, join as join2 } from "path";
11
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
10
12
  import ora from "ora";
11
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
12
- import { dirname as dirname2, join as join2 } from "path";
13
-
14
- // src/utils/logger.ts
15
- import pc from "picocolors";
16
- var ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
17
- function visibleLength(text) {
18
- return text.replace(ANSI_PATTERN, "").length;
19
- }
20
- function padVisible(text, width) {
21
- const padding = Math.max(0, width - visibleLength(text));
22
- return text + " ".repeat(padding);
23
- }
24
- function box(lines, color = pc.green) {
25
- const contentWidth = Math.max(...lines.map((line) => visibleLength(line)), 0);
26
- const top = color(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`);
27
- const bottom = color(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`);
28
- console.log(top);
29
- for (const line of lines) {
30
- console.log(color("\u2502 ") + padVisible(line, contentWidth) + color(" \u2502"));
31
- }
32
- console.log(bottom);
33
- }
34
- var log = {
35
- info: (message) => console.log(pc.cyan(message)),
36
- success: (message) => console.log(pc.green(`\u2714 ${message}`)),
37
- warn: (message) => console.log(pc.yellow(`\u26A0 ${message}`)),
38
- error: (message) => console.log(pc.red(`\u2716 ${message}`)),
39
- dim: (message) => console.log(pc.dim(message)),
40
- item: (message) => console.log(pc.green(` ${message}`)),
41
- itemAdd: (message) => console.log(` ${pc.green("+")} ${message}`),
42
- plain: (message) => console.log(message),
43
- blank: () => console.log(""),
44
- box
45
- };
46
-
47
- // src/utils/prompts.ts
48
- import pc2 from "picocolors";
49
- import { checkbox } from "@inquirer/prompts";
50
- import readline from "readline";
51
- async function checkboxWithHover(config, options) {
52
- const choices = config.choices.filter(
53
- (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
54
- );
55
- const values = choices.map((c) => c.value);
56
- const totalItems = values.length;
57
- let cursorPosition = choices.findIndex((c) => !c.disabled);
58
- if (cursorPosition < 0) cursorPosition = 0;
59
- const getName = options?.getName ?? ((v) => v.name);
60
- const keypressHandler = (_str, key) => {
61
- if (key.name === "up") {
62
- let next = cursorPosition - 1;
63
- while (next >= 0 && choices[next].disabled) next--;
64
- if (next >= 0) cursorPosition = next;
65
- } else if (key.name === "down") {
66
- let next = cursorPosition + 1;
67
- while (next < totalItems && choices[next].disabled) next++;
68
- if (next < totalItems) cursorPosition = next;
69
- }
70
- };
71
- readline.emitKeypressEvents(process.stdin);
72
- process.stdin.on("keypress", keypressHandler);
73
- const customConfig = {
74
- ...config,
75
- theme: {
76
- ...config.theme,
77
- style: {
78
- answer: (text) => pc2.green(text),
79
- ...config.theme?.style,
80
- highlight: (text) => pc2.green(text),
81
- renderSelectedChoices: (selected, _allChoices) => {
82
- if (selected.length === 0) {
83
- return pc2.dim(getName(values[cursorPosition]));
84
- }
85
- return selected.map((c) => getName(c.value)).join(", ");
86
- }
87
- }
88
- }
89
- };
90
- try {
91
- const selected = await checkbox(customConfig);
92
- if (selected.length === 0) {
93
- return [values[cursorPosition]];
94
- }
95
- return selected;
96
- } finally {
97
- process.stdin.removeListener("keypress", keypressHandler);
98
- }
99
- }
100
-
101
- // src/utils/tracking.ts
102
- function trackEvent(_event, _data) {
103
- }
13
+ import pc3 from "picocolors";
104
14
 
105
- // src/setup/http-agents.ts
15
+ // src/setup/agents.ts
106
16
  import { access } from "fs/promises";
107
- import { homedir } from "os";
108
17
  import { join } from "path";
18
+ import { homedir } from "os";
109
19
  var SETUP_AGENT_NAMES = {
110
20
  claude: "Claude Code",
111
21
  cursor: "Cursor",
@@ -114,19 +24,39 @@ var SETUP_AGENT_NAMES = {
114
24
  antigravity: "Antigravity",
115
25
  gemini: "Gemini CLI"
116
26
  };
117
- var mcpBaseUrl = "https://www.genrtl.com/api/mcp";
27
+ var mcpBaseUrl = "https://genrtl.com/api/mcp";
28
+ var STDIO_PACKAGE = "@upstash/genrtl-mcp";
118
29
  function setMcpBaseUrl(url) {
119
30
  const normalized = url.replace(/\/+$/, "");
120
31
  mcpBaseUrl = normalized.endsWith("/api/mcp") ? normalized : `${normalized}/api/mcp`;
121
32
  }
122
- function headers(auth) {
123
- return { Authorization: `Bearer ${auth.apiKey}` };
33
+ function stdioArgs(auth) {
34
+ const args = ["-y", STDIO_PACKAGE];
35
+ if (auth.mode === "api-key" && auth.apiKey) {
36
+ args.push("--api-key", auth.apiKey);
37
+ }
38
+ return args;
39
+ }
40
+ function stdioEntry(auth) {
41
+ return { command: "npx", args: stdioArgs(auth) };
124
42
  }
125
43
  function claudeConfigDir() {
126
44
  return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
127
45
  }
128
46
  function claudeGlobalMcpPath() {
129
- return process.env.CLAUDE_CONFIG_DIR ? join(claudeConfigDir(), ".claude.json") : join(homedir(), ".claude.json");
47
+ if (process.env.CLAUDE_CONFIG_DIR) {
48
+ return join(claudeConfigDir(), ".claude.json");
49
+ }
50
+ return join(homedir(), ".claude.json");
51
+ }
52
+ function mcpUrl(_auth) {
53
+ return mcpBaseUrl;
54
+ }
55
+ function withHeaders(base, auth) {
56
+ if (auth.mode === "api-key" && auth.apiKey) {
57
+ return { ...base, headers: { Authorization: `Bearer ${auth.apiKey}` } };
58
+ }
59
+ return base;
130
60
  }
131
61
  var agents = {
132
62
  claude: {
@@ -138,17 +68,17 @@ var agents = {
138
68
  return [claudeGlobalMcpPath()];
139
69
  },
140
70
  configKey: "mcpServers",
141
- buildEntry: (auth) => ({
142
- type: "http",
143
- url: mcpBaseUrl,
144
- headers: headers(auth)
145
- })
71
+ buildEntry: (auth, transport) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ type: "http", url: mcpUrl(auth) }, auth)
146
72
  },
147
73
  rule: {
148
74
  kind: "file",
149
75
  dir: (scope) => scope === "global" ? join(claudeConfigDir(), "rules") : join(".claude", "rules"),
150
76
  filename: "genrtl.md"
151
77
  },
78
+ skill: {
79
+ name: "genrtl-mcp",
80
+ dir: (scope) => scope === "global" ? join(claudeConfigDir(), "skills") : join(".claude", "skills")
81
+ },
152
82
  detect: {
153
83
  projectPaths: [".mcp.json", ".claude"],
154
84
  get globalPaths() {
@@ -163,13 +93,17 @@ var agents = {
163
93
  projectPaths: [join(".cursor", "mcp.json")],
164
94
  globalPaths: [join(homedir(), ".cursor", "mcp.json")],
165
95
  configKey: "mcpServers",
166
- buildEntry: (auth) => ({ url: mcpBaseUrl, headers: headers(auth) })
96
+ buildEntry: (auth, transport) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ url: mcpUrl(auth) }, auth)
167
97
  },
168
98
  rule: {
169
99
  kind: "file",
170
100
  dir: (scope) => scope === "global" ? join(homedir(), ".cursor", "rules") : join(".cursor", "rules"),
171
101
  filename: "genrtl.mdc"
172
102
  },
103
+ skill: {
104
+ name: "genrtl-mcp",
105
+ dir: (scope) => scope === "global" ? join(homedir(), ".cursor", "skills") : join(".cursor", "skills")
106
+ },
173
107
  detect: {
174
108
  projectPaths: [".cursor"],
175
109
  globalPaths: [join(homedir(), ".cursor")]
@@ -187,18 +121,17 @@ var agents = {
187
121
  join(homedir(), ".config", "opencode", ".opencode.jsonc")
188
122
  ],
189
123
  configKey: "mcp",
190
- buildEntry: (auth) => ({
191
- type: "remote",
192
- url: mcpBaseUrl,
193
- enabled: true,
194
- headers: headers(auth)
195
- })
124
+ buildEntry: (auth, transport) => transport === "stdio" ? { type: "local", command: ["npx", ...stdioArgs(auth)], enabled: true } : withHeaders({ type: "remote", url: mcpUrl(auth), enabled: true }, auth)
196
125
  },
197
126
  rule: {
198
127
  kind: "append",
199
128
  file: (scope) => scope === "global" ? join(homedir(), ".config", "opencode", "AGENTS.md") : "AGENTS.md",
200
129
  sectionMarker: "<!-- genrtl -->"
201
130
  },
131
+ skill: {
132
+ name: "genrtl-mcp",
133
+ dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
134
+ },
202
135
  detect: {
203
136
  projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
204
137
  globalPaths: [join(homedir(), ".config", "opencode")]
@@ -211,14 +144,16 @@ var agents = {
211
144
  projectPaths: [join(".codex", "config.toml")],
212
145
  globalPaths: [join(homedir(), ".codex", "config.toml")],
213
146
  configKey: "mcp_servers",
214
- buildEntry: (auth) => auth.apiKeyEnvVar ? {
215
- type: "http",
216
- url: mcpBaseUrl,
217
- bearer_token_env_var: auth.apiKeyEnvVar
218
- } : {
219
- type: "http",
220
- url: mcpBaseUrl,
221
- headers: headers(auth)
147
+ buildEntry: (auth, transport) => {
148
+ if (transport === "stdio") return stdioEntry(auth);
149
+ if (auth.mode === "api-key" && auth.apiKeyEnvVar) {
150
+ return {
151
+ type: "http",
152
+ url: mcpUrl(auth),
153
+ bearer_token_env_var: auth.apiKeyEnvVar
154
+ };
155
+ }
156
+ return withHeaders({ type: "http", url: mcpUrl(auth) }, auth);
222
157
  }
223
158
  },
224
159
  rule: {
@@ -226,25 +161,37 @@ var agents = {
226
161
  file: (scope) => scope === "global" ? join(homedir(), ".codex", "AGENTS.md") : "AGENTS.md",
227
162
  sectionMarker: "<!-- genrtl -->"
228
163
  },
164
+ skill: {
165
+ name: "genrtl-mcp",
166
+ dir: (scope) => scope === "global" ? join(homedir(), ".agents", "skills") : join(".agents", "skills")
167
+ },
229
168
  detect: {
230
169
  projectPaths: [".codex"],
231
170
  globalPaths: [join(homedir(), ".codex")]
232
171
  }
233
172
  },
173
+ // Antigravity is built on Gemini infrastructure and shares ~/.gemini/. Per
174
+ // the official Codelabs guide, Antigravity 2.0/IDE/CLI read MCP servers from
175
+ // ~/.gemini/config/mcp_config.json globally; there is no project-level MCP
176
+ // config, so projectPaths is empty and setupAgent falls back to global.
234
177
  antigravity: {
235
178
  name: "antigravity",
236
179
  displayName: "Antigravity",
237
180
  mcp: {
238
181
  projectPaths: [],
239
- globalPaths: [join(homedir(), ".gemini", "antigravity", "mcp_config.json")],
182
+ globalPaths: [join(homedir(), ".gemini", "config", "mcp_config.json")],
240
183
  configKey: "mcpServers",
241
- buildEntry: (auth) => ({ serverUrl: mcpBaseUrl, headers: headers(auth) })
184
+ buildEntry: (auth, transport) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ serverUrl: mcpUrl(auth) }, auth)
242
185
  },
243
186
  rule: {
244
187
  kind: "append",
245
188
  file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
246
189
  sectionMarker: "<!-- genrtl -->"
247
190
  },
191
+ skill: {
192
+ name: "genrtl-mcp",
193
+ dir: (scope) => scope === "global" ? join(homedir(), ".agent", "skills") : join(".agent", "skills")
194
+ },
248
195
  detect: {
249
196
  projectPaths: [".agent"],
250
197
  globalPaths: [join(homedir(), ".gemini", "antigravity"), join(homedir(), ".agent")]
@@ -257,13 +204,17 @@ var agents = {
257
204
  projectPaths: [join(".gemini", "settings.json")],
258
205
  globalPaths: [join(homedir(), ".gemini", "settings.json")],
259
206
  configKey: "mcpServers",
260
- buildEntry: (auth) => ({ httpUrl: mcpBaseUrl, headers: headers(auth) })
207
+ buildEntry: (auth, transport) => transport === "stdio" ? stdioEntry(auth) : withHeaders({ httpUrl: mcpUrl(auth) }, auth)
261
208
  },
262
209
  rule: {
263
210
  kind: "append",
264
211
  file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
265
212
  sectionMarker: "<!-- genrtl -->"
266
213
  },
214
+ skill: {
215
+ name: "genrtl-mcp",
216
+ dir: (scope) => scope === "global" ? join(homedir(), ".gemini", "skills") : join(".gemini", "skills")
217
+ },
267
218
  detect: {
268
219
  projectPaths: [".gemini"],
269
220
  globalPaths: [join(homedir(), ".gemini")]
@@ -274,9 +225,9 @@ function getAgent(name) {
274
225
  return agents[name];
275
226
  }
276
227
  var ALL_AGENT_NAMES = Object.keys(agents);
277
- async function pathExists(path) {
228
+ async function pathExists(p) {
278
229
  try {
279
- await access(path);
230
+ await access(p);
280
231
  return true;
281
232
  } catch {
282
233
  return false;
@@ -286,8 +237,8 @@ async function detectAgents(scope) {
286
237
  const detected = [];
287
238
  for (const agent of Object.values(agents)) {
288
239
  const paths = scope === "global" ? agent.detect.globalPaths : agent.detect.projectPaths;
289
- for (const path of paths) {
290
- const fullPath = scope === "global" ? path : join(process.cwd(), path);
240
+ for (const p of paths) {
241
+ const fullPath = scope === "global" ? p : join(process.cwd(), p);
291
242
  if (await pathExists(fullPath)) {
292
243
  detected.push(agent.name);
293
244
  break;
@@ -297,56 +248,6 @@ async function detectAgents(scope) {
297
248
  return detected;
298
249
  }
299
250
 
300
- // src/setup/templates.ts
301
- var GITHUB_RAW_URLS = ["https://raw.githubusercontent.com/xroting/grtl/main/rules"];
302
- var FALLBACK_MCP = `Use GenRTL MCP tools for grounded RTL engineering knowledge.
303
-
304
- Choose one tool:
305
- - \`genrtl_knowledge_search\` for cross-domain RTL questions
306
- - \`genrtl_spec2rtl_search\` for requirements and RTL design
307
- - \`genrtl_verification_search\` for testbenches and verification
308
- - \`genrtl_debug_search\` for lint, CDC, compile, synthesis, and RTL bugs
309
-
310
- Pass the complete engineering question in \`query\`. Add filters only when useful.`;
311
- var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge.
312
-
313
- Choose one command:
314
- - \`npx @genrtl/grtl@latest knowledge-search "<query>"\`
315
- - \`npx @genrtl/grtl@latest spec2rtl-search "<query>"\`
316
- - \`npx @genrtl/grtl@latest verification-search "<query>"\`
317
- - \`npx @genrtl/grtl@latest debug-search "<query>"\`
318
-
319
- Pass the complete engineering question. Add filters such as \`--tool\`,
320
- \`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` when known.
321
- If authentication fails, set \`GRTL_API_KEY\`.`;
322
- var CURSOR_FRONTMATTER = `---
323
- alwaysApply: true
324
- ---
325
-
326
- `;
327
- var CODEX_CLI_SANDBOX_GUIDANCE = `Run GenRTL CLI requests outside Codex's default sandbox. If a GenRTL CLI command fails with DNS or network errors such as ENOTFOUND, host resolution failures, or fetch failed, rerun it outside the sandbox instead of retrying inside the sandbox.`;
328
- async function fetchRule(filename, fallback) {
329
- for (const base of GITHUB_RAW_URLS) {
330
- try {
331
- const res = await fetch(`${base}/${filename}`);
332
- if (res.ok) return await res.text();
333
- } catch {
334
- continue;
335
- }
336
- }
337
- return fallback;
338
- }
339
- async function getRuleContent(mode, agent) {
340
- const [filename, fallback] = mode === "mcp" ? ["genrtl-mcp.md", FALLBACK_MCP] : ["genrtl-cli.md", FALLBACK_CLI];
341
- let body = await fetchRule(filename, fallback);
342
- if (mode === "cli" && agent === "codex" && !body.includes(CODEX_CLI_SANDBOX_GUIDANCE)) {
343
- body = `${body.trimEnd()}
344
- ${CODEX_CLI_SANDBOX_GUIDANCE}
345
- `;
346
- }
347
- return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
348
- }
349
-
350
251
  // src/setup/mcp-writer.ts
351
252
  import { access as access2, readFile, writeFile, mkdir } from "fs/promises";
352
253
  import { dirname } from "path";
@@ -415,15 +316,15 @@ async function writeJsonConfig(filePath, config) {
415
316
  }
416
317
  function buildTomlServerBlock(serverName, entry) {
417
318
  const lines = [`[mcp_servers.${serverName}]`];
418
- const headers2 = entry.headers;
319
+ const headers = entry.headers;
419
320
  for (const [key, value] of Object.entries(entry)) {
420
321
  if (key === "headers") continue;
421
322
  lines.push(`${key} = ${JSON.stringify(value)}`);
422
323
  }
423
- if (headers2 && Object.keys(headers2).length > 0) {
324
+ if (headers && Object.keys(headers).length > 0) {
424
325
  lines.push("");
425
326
  lines.push(`[mcp_servers.${serverName}.http_headers]`);
426
- for (const [key, value] of Object.entries(headers2)) {
327
+ for (const [key, value] of Object.entries(headers)) {
427
328
  lines.push(`${key} = ${JSON.stringify(value)}`);
428
329
  }
429
330
  }
@@ -468,6 +369,232 @@ async function appendTomlServer(filePath, serverName, entry) {
468
369
  return { alreadyExists };
469
370
  }
470
371
 
372
+ // src/setup/templates.ts
373
+ var GITHUB_RAW_URLS = ["https://raw.githubusercontent.com/xroting/grtl/main/rules"];
374
+ var FALLBACK_MCP = `Use GenRTL MCP tools for grounded RTL engineering knowledge.
375
+
376
+ Choose one tool:
377
+ - \`genrtl_knowledge_search\` for cross-domain RTL questions
378
+ - \`genrtl_spec2rtl_search\` for requirements and RTL design
379
+ - \`genrtl_verification_search\` for testbenches and verification
380
+ - \`genrtl_debug_search\` for lint, CDC, compile, synthesis, and RTL bugs
381
+
382
+ Pass the complete engineering question in \`query\`. Add filters only when useful.`;
383
+ var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge.
384
+
385
+ Choose one command:
386
+ - \`npx @genrtl/grtl@latest knowledge-search "<query>"\`
387
+ - \`npx @genrtl/grtl@latest spec2rtl-search "<query>"\`
388
+ - \`npx @genrtl/grtl@latest verification-search "<query>"\`
389
+ - \`npx @genrtl/grtl@latest debug-search "<query>"\`
390
+
391
+ Pass the complete engineering question. Add filters such as \`--tool\`,
392
+ \`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` when known.
393
+ If authentication fails, set \`GRTL_API_KEY\` or \`GENRTL_API_KEY\` in the
394
+ coding agent's environment.`;
395
+ var CURSOR_FRONTMATTER = `---
396
+ alwaysApply: true
397
+ ---
398
+
399
+ `;
400
+ var CODEX_CLI_SANDBOX_GUIDANCE = `Run GenRTL CLI requests outside Codex's default sandbox. If a GenRTL CLI command fails with DNS or network errors such as ENOTFOUND, host resolution failures, or fetch failed, rerun it outside the sandbox instead of retrying inside the sandbox.`;
401
+ var MCP_SKILL = `---
402
+ name: genrtl-mcp
403
+ description: Use GenRTL MCP tools for grounded RTL design, verification, lint, CDC, synthesis, compile, and debugging knowledge.
404
+ ---
405
+
406
+ # GenRTL MCP
407
+
408
+ Use this skill when an RTL engineering task needs grounded GenRTL knowledge.
409
+
410
+ Choose exactly one MCP tool:
411
+
412
+ - \`genrtl_knowledge_search\` for cross-domain RTL questions.
413
+ - \`genrtl_spec2rtl_search\` for requirements, protocols, control logic, or algorithm-to-RTL work.
414
+ - \`genrtl_verification_search\` for testbenches and verification.
415
+ - \`genrtl_debug_search\` for lint, CDC, compile, synthesis, or RTL bugs.
416
+
417
+ Pass the complete engineering question in \`query\`. Add \`filters\`, \`top_k\`,
418
+ \`min_score\`, or \`workspace_id\` only when useful.
419
+ `;
420
+ var CLI_SKILL = `---
421
+ name: genrtl-cli
422
+ description: Use the grtl CLI for grounded RTL design, verification, lint, CDC, synthesis, compile, and debugging knowledge.
423
+ ---
424
+
425
+ # GenRTL CLI
426
+
427
+ Use this skill when an RTL engineering task needs grounded GenRTL knowledge and
428
+ the GenRTL MCP server is not configured.
429
+
430
+ Choose exactly one command:
431
+
432
+ - \`grtl knowledge-search "<query>" --json\` for cross-domain RTL questions.
433
+ - \`grtl spec2rtl-search "<query>" --json\` for requirements, protocols, control logic, or algorithm-to-RTL work.
434
+ - \`grtl verification-search "<query>" --json\` for testbenches and verification.
435
+ - \`grtl debug-search "<query>" --json\` for lint, CDC, compile, synthesis, or RTL bugs.
436
+
437
+ Pass the complete engineering question. Add filters such as \`--tool\`,
438
+ \`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` only when useful.
439
+ The CLI requires \`GRTL_API_KEY\` or \`GENRTL_API_KEY\` in its environment.
440
+ `;
441
+ function getSkillContent(mode) {
442
+ return mode === "mcp" ? MCP_SKILL : CLI_SKILL;
443
+ }
444
+ async function fetchRule(filename, fallback) {
445
+ for (const base of GITHUB_RAW_URLS) {
446
+ try {
447
+ const res = await fetch(`${base}/${filename}`);
448
+ if (res.ok) return await res.text();
449
+ } catch {
450
+ continue;
451
+ }
452
+ }
453
+ return fallback;
454
+ }
455
+ async function getRuleContent(mode, agent) {
456
+ const [filename, fallback] = mode === "mcp" ? ["genrtl-mcp.md", FALLBACK_MCP] : ["genrtl-cli.md", FALLBACK_CLI];
457
+ let body = await fetchRule(filename, fallback);
458
+ if (mode === "cli" && agent === "codex" && !body.includes(CODEX_CLI_SANDBOX_GUIDANCE)) {
459
+ body = `${body.trimEnd()}
460
+ ${CODEX_CLI_SANDBOX_GUIDANCE}
461
+ `;
462
+ }
463
+ return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
464
+ }
465
+
466
+ // src/utils/installer.ts
467
+ import { mkdir as mkdir2, writeFile as writeFile2, rm, symlink, lstat } from "fs/promises";
468
+ import { resolve as resolve2, dirname as dirname3 } from "path";
469
+
470
+ // src/utils/skill-name.ts
471
+ import { resolve, dirname as dirname2, basename } from "path";
472
+ var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
473
+ function isSafeSkillName(name) {
474
+ if (typeof name !== "string") return false;
475
+ if (name.length === 0 || name.length > 128) return false;
476
+ if (name === "." || name === "..") return false;
477
+ if (name.includes("\0")) return false;
478
+ if (!SAFE_NAME.test(name)) return false;
479
+ return true;
480
+ }
481
+ function assertSkillNameInRoot(skillsRoot, skillName) {
482
+ if (!isSafeSkillName(skillName)) {
483
+ throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);
484
+ }
485
+ const root = resolve(skillsRoot);
486
+ const target = resolve(root, skillName);
487
+ if (dirname2(target) !== root || basename(target) !== skillName) {
488
+ throw new Error(`Skill name "${skillName}" escapes the skills root`);
489
+ }
490
+ return target;
491
+ }
492
+
493
+ // src/utils/installer.ts
494
+ async function installSkillFiles(skillName, files, skillsRoot) {
495
+ const skillDir = assertSkillNameInRoot(skillsRoot, skillName);
496
+ for (const file of files) {
497
+ const filePath = resolve2(skillDir, file.path);
498
+ if (!filePath.startsWith(skillDir + "/") && !filePath.startsWith(skillDir + "\\") && filePath !== skillDir) {
499
+ throw new Error(`Skill file path "${file.path}" resolves outside the target directory`);
500
+ }
501
+ const fileDir = dirname3(filePath);
502
+ await mkdir2(fileDir, { recursive: true });
503
+ await writeFile2(filePath, file.content);
504
+ }
505
+ }
506
+
507
+ // src/utils/logger.ts
508
+ import pc from "picocolors";
509
+ var ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
510
+ function visibleLength(text) {
511
+ return text.replace(ANSI_PATTERN, "").length;
512
+ }
513
+ function padVisible(text, width) {
514
+ const padding = Math.max(0, width - visibleLength(text));
515
+ return text + " ".repeat(padding);
516
+ }
517
+ function box(lines, color = pc.green) {
518
+ const contentWidth = Math.max(...lines.map((line) => visibleLength(line)), 0);
519
+ const top = color(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`);
520
+ const bottom = color(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`);
521
+ console.log(top);
522
+ for (const line of lines) {
523
+ console.log(color("\u2502 ") + padVisible(line, contentWidth) + color(" \u2502"));
524
+ }
525
+ console.log(bottom);
526
+ }
527
+ var log = {
528
+ info: (message) => console.log(pc.cyan(message)),
529
+ success: (message) => console.log(pc.green(`\u2714 ${message}`)),
530
+ warn: (message) => console.log(pc.yellow(`\u26A0 ${message}`)),
531
+ error: (message) => console.log(pc.red(`\u2716 ${message}`)),
532
+ dim: (message) => console.log(pc.dim(message)),
533
+ item: (message) => console.log(pc.green(` ${message}`)),
534
+ itemAdd: (message) => console.log(` ${pc.green("+")} ${message}`),
535
+ plain: (message) => console.log(message),
536
+ blank: () => console.log(""),
537
+ box
538
+ };
539
+
540
+ // src/utils/prompts.ts
541
+ import pc2 from "picocolors";
542
+ import { checkbox } from "@inquirer/prompts";
543
+ import readline from "readline";
544
+ async function checkboxWithHover(config, options) {
545
+ const choices = config.choices.filter(
546
+ (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
547
+ );
548
+ const values = choices.map((c) => c.value);
549
+ const totalItems = values.length;
550
+ let cursorPosition = choices.findIndex((c) => !c.disabled);
551
+ if (cursorPosition < 0) cursorPosition = 0;
552
+ const getName = options?.getName ?? ((v) => v.name);
553
+ const keypressHandler = (_str, key) => {
554
+ if (key.name === "up") {
555
+ let next = cursorPosition - 1;
556
+ while (next >= 0 && choices[next].disabled) next--;
557
+ if (next >= 0) cursorPosition = next;
558
+ } else if (key.name === "down") {
559
+ let next = cursorPosition + 1;
560
+ while (next < totalItems && choices[next].disabled) next++;
561
+ if (next < totalItems) cursorPosition = next;
562
+ }
563
+ };
564
+ readline.emitKeypressEvents(process.stdin);
565
+ process.stdin.on("keypress", keypressHandler);
566
+ const customConfig = {
567
+ ...config,
568
+ theme: {
569
+ ...config.theme,
570
+ style: {
571
+ answer: (text) => pc2.green(text),
572
+ ...config.theme?.style,
573
+ highlight: (text) => pc2.green(text),
574
+ renderSelectedChoices: (selected, _allChoices) => {
575
+ if (selected.length === 0) {
576
+ return pc2.dim(getName(values[cursorPosition]));
577
+ }
578
+ return selected.map((c) => getName(c.value)).join(", ");
579
+ }
580
+ }
581
+ }
582
+ };
583
+ try {
584
+ const selected = await checkbox(customConfig);
585
+ if (selected.length === 0) {
586
+ return [values[cursorPosition]];
587
+ }
588
+ return selected;
589
+ } finally {
590
+ process.stdin.removeListener("keypress", keypressHandler);
591
+ }
592
+ }
593
+
594
+ // src/utils/tracking.ts
595
+ function trackEvent(_event, _data) {
596
+ }
597
+
471
598
  // src/commands/setup.ts
472
599
  var CHECKBOX_THEME = {
473
600
  style: {
@@ -475,6 +602,10 @@ var CHECKBOX_THEME = {
475
602
  disabledChoice: (text) => ` ${pc3.dim("-")} ${pc3.dim(text)}`
476
603
  }
477
604
  };
605
+ var SKILL_NAMES = {
606
+ cli: "genrtl-cli",
607
+ mcp: "genrtl-mcp"
608
+ };
478
609
  function getSelectedAgents(options) {
479
610
  const agents2 = [];
480
611
  if (options.claude) agents2.push("claude");
@@ -486,20 +617,57 @@ function getSelectedAgents(options) {
486
617
  return agents2;
487
618
  }
488
619
  function resolveSetupAuth(options) {
489
- if (options.apiKey) return { apiKey: options.apiKey };
620
+ if (options.apiKey) return { mode: "api-key", apiKey: options.apiKey };
490
621
  if (process.env.GRTL_API_KEY) {
491
- return { apiKey: process.env.GRTL_API_KEY, apiKeyEnvVar: "GRTL_API_KEY" };
622
+ return {
623
+ mode: "api-key",
624
+ apiKey: process.env.GRTL_API_KEY,
625
+ apiKeyEnvVar: "GRTL_API_KEY"
626
+ };
492
627
  }
493
628
  if (process.env.GENRTL_API_KEY) {
494
- return { apiKey: process.env.GENRTL_API_KEY, apiKeyEnvVar: "GENRTL_API_KEY" };
629
+ return {
630
+ mode: "api-key",
631
+ apiKey: process.env.GENRTL_API_KEY,
632
+ apiKeyEnvVar: "GENRTL_API_KEY"
633
+ };
495
634
  }
496
635
  return void 0;
497
636
  }
498
637
  function registerSetupCommand(program2) {
499
- program2.command("setup").description("Configure the hosted GenRTL HTTP MCP server for a coding agent").option("--claude", "Set up Claude Code").option("--cursor", "Set up Cursor").option("--antigravity", "Set up Antigravity").option("--opencode", "Set up OpenCode").option("--codex", "Set up Codex").option("--gemini", "Set up Gemini CLI").option("-p, --project", "Configure the current project instead of the user profile").option("-y, --yes", "Use all detected agents without prompting").option("--api-key <key>", "GenRTL API key (prefer the GRTL_API_KEY environment variable)").action(async (options) => {
638
+ program2.command("setup").description("Install GenRTL CLI or MCP integration for a coding agent").option("--cli", "Install a Skill that calls the grtl CLI").option("--mcp", "Configure GenRTL MCP and install its Skill").option("--claude", "Set up Claude Code").option("--cursor", "Set up Cursor").option("--antigravity", "Set up Antigravity").option("--opencode", "Set up OpenCode").option("--codex", "Set up Codex").option("--gemini", "Set up Gemini CLI").option("-p, --project", "Configure the current project instead of the user profile").option("-y, --yes", "Use MCP mode and all detected agents without prompting").option("--api-key <key>", "GenRTL API key for MCP mode").action(async (options) => {
500
639
  await setupCommand(options);
501
640
  });
502
641
  }
642
+ async function resolveMode(options) {
643
+ if (options.cli && options.mcp) {
644
+ log.error("Choose either --cli or --mcp, not both.");
645
+ process.exitCode = 1;
646
+ return null;
647
+ }
648
+ if (options.cli) return "cli";
649
+ if (options.mcp || options.yes) return "mcp";
650
+ try {
651
+ return await select({
652
+ message: "How should your coding agent access GenRTL?",
653
+ choices: [
654
+ {
655
+ name: "CLI Skill",
656
+ value: "cli",
657
+ description: "The agent runs the installed grtl command."
658
+ },
659
+ {
660
+ name: "MCP Server + Skill",
661
+ value: "mcp",
662
+ description: "The agent calls the four hosted GenRTL MCP tools."
663
+ }
664
+ ]
665
+ });
666
+ } catch {
667
+ log.warn("Setup cancelled");
668
+ return null;
669
+ }
670
+ }
503
671
  async function promptAgents() {
504
672
  try {
505
673
  return await checkboxWithHover(
@@ -524,7 +692,7 @@ async function resolveAgents(options, scope) {
524
692
  const detected = await detectAgents(scope);
525
693
  if (detected.length > 0 && options.yes) return detected;
526
694
  if (options.yes) {
527
- log.error("No supported coding agents were detected. Pass an agent flag such as --cursor.");
695
+ log.error("No supported coding agents were detected. Pass an agent flag such as --codex.");
528
696
  return [];
529
697
  }
530
698
  log.blank();
@@ -532,14 +700,14 @@ async function resolveAgents(options, scope) {
532
700
  if (!selected) log.warn("Setup cancelled");
533
701
  return selected ?? [];
534
702
  }
535
- async function installRule(agentName, scope) {
703
+ async function installRule(agentName, scope, mode) {
536
704
  const rule = getAgent(agentName).rule;
537
- const content = await getRuleContent("mcp", agentName);
705
+ const content = await getRuleContent(mode, agentName);
538
706
  if (rule.kind === "file") {
539
707
  const ruleDir = scope === "global" ? rule.dir("global") : join2(process.cwd(), rule.dir("project"));
540
708
  const rulePath = join2(ruleDir, rule.filename);
541
- await mkdir2(dirname2(rulePath), { recursive: true });
542
- await writeFile2(rulePath, content, "utf-8");
709
+ await mkdir3(dirname4(rulePath), { recursive: true });
710
+ await writeFile3(rulePath, content, "utf-8");
543
711
  return { status: "installed", path: rulePath };
544
712
  }
545
713
  const filePath = scope === "global" ? rule.file("global") : join2(process.cwd(), rule.file("project"));
@@ -554,69 +722,108 @@ ${rule.sectionMarker}`;
554
722
  }
555
723
  if (existing.includes(rule.sectionMarker)) {
556
724
  const regex = new RegExp(`${escapedMarker}\\n[\\s\\S]*?${escapedMarker}`);
557
- await writeFile2(filePath, existing.replace(regex, section), "utf-8");
725
+ await writeFile3(filePath, existing.replace(regex, section), "utf-8");
558
726
  return { status: "updated", path: filePath };
559
727
  }
560
728
  const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
561
- await mkdir2(dirname2(filePath), { recursive: true });
562
- await writeFile2(filePath, `${existing}${separator}${section}
729
+ await mkdir3(dirname4(filePath), { recursive: true });
730
+ await writeFile3(filePath, `${existing}${separator}${section}
563
731
  `, "utf-8");
564
732
  return { status: "installed", path: filePath };
565
733
  }
566
- async function setupAgent(agentName, auth, scope) {
734
+ async function installSkill(agentName, scope, mode) {
735
+ const agent = getAgent(agentName);
736
+ const skillName = SKILL_NAMES[mode];
737
+ const skillsRoot = scope === "global" ? agent.skill.dir("global") : join2(process.cwd(), agent.skill.dir("project"));
738
+ const skillPath = join2(skillsRoot, skillName);
739
+ await installSkillFiles(
740
+ skillName,
741
+ [{ path: "SKILL.md", content: getSkillContent(mode) }],
742
+ skillsRoot
743
+ );
744
+ return { status: "installed", path: skillPath };
745
+ }
746
+ async function configureMcp(agentName, auth, scope) {
567
747
  const agent = getAgent(agentName);
568
748
  const candidates = scope === "global" || agent.mcp.projectPaths.length === 0 ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((path) => join2(process.cwd(), path));
569
749
  const mcpPath = await resolveMcpPath(candidates);
570
- const entry = agent.mcp.buildEntry(auth);
750
+ const entry = agent.mcp.buildEntry(auth, "http");
751
+ if (mcpPath.endsWith(".toml")) {
752
+ const { alreadyExists: alreadyExists2 } = await appendTomlServer(mcpPath, "genrtl", entry);
753
+ return { status: alreadyExists2 ? "reconfigured" : "configured", path: mcpPath };
754
+ }
755
+ const existing = await readJsonConfig(mcpPath);
756
+ const { config, alreadyExists } = mergeServerEntry(
757
+ existing,
758
+ agent.mcp.configKey,
759
+ "genrtl",
760
+ entry
761
+ );
762
+ await writeJsonConfig(mcpPath, config);
763
+ return { status: alreadyExists ? "reconfigured" : "configured", path: mcpPath };
764
+ }
765
+ async function setupAgent(agentName, mode, auth, scope) {
766
+ const agent = getAgent(agentName);
571
767
  let mcpStatus;
572
- try {
573
- if (mcpPath.endsWith(".toml")) {
574
- const { alreadyExists } = await appendTomlServer(mcpPath, "genrtl", entry);
575
- mcpStatus = alreadyExists ? "reconfigured" : "configured";
576
- } else {
577
- const existing = await readJsonConfig(mcpPath);
578
- const { config, alreadyExists } = mergeServerEntry(
579
- existing,
580
- agent.mcp.configKey,
581
- "genrtl",
582
- entry
583
- );
584
- await writeJsonConfig(mcpPath, config);
585
- mcpStatus = alreadyExists ? "reconfigured" : "configured";
768
+ let mcpPath;
769
+ if (mode === "mcp" && auth) {
770
+ try {
771
+ const result = await configureMcp(agentName, auth, scope);
772
+ mcpStatus = result.status;
773
+ mcpPath = result.path;
774
+ } catch (error) {
775
+ mcpStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
586
776
  }
587
- } catch (error) {
588
- mcpStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
589
777
  }
590
778
  let ruleStatus;
591
779
  let rulePath = "";
592
780
  try {
593
- const result = await installRule(agentName, scope);
781
+ const result = await installRule(agentName, scope, mode);
594
782
  ruleStatus = result.status;
595
783
  rulePath = result.path;
596
784
  } catch (error) {
597
785
  ruleStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
598
786
  }
599
- return { agent: agent.displayName, mcpStatus, mcpPath, ruleStatus, rulePath };
787
+ let skillStatus;
788
+ let skillPath = "";
789
+ try {
790
+ const result = await installSkill(agentName, scope, mode);
791
+ skillStatus = result.status;
792
+ skillPath = result.path;
793
+ } catch (error) {
794
+ skillStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
795
+ }
796
+ return {
797
+ agent: agent.displayName,
798
+ mcpStatus,
799
+ mcpPath,
800
+ ruleStatus,
801
+ rulePath,
802
+ skillStatus,
803
+ skillPath
804
+ };
600
805
  }
601
806
  async function setupCommand(options) {
602
807
  trackEvent("command", { name: "setup" });
808
+ const mode = await resolveMode(options);
809
+ if (!mode) return;
603
810
  const auth = resolveSetupAuth(options);
604
- if (!auth) {
605
- log.error("Set GRTL_API_KEY or pass --api-key before running setup.");
811
+ if (mode === "mcp" && !auth) {
812
+ log.error("MCP mode requires GRTL_API_KEY, GENRTL_API_KEY, or --api-key.");
606
813
  process.exitCode = 1;
607
814
  return;
608
815
  }
609
816
  const scope = options.project ? "project" : "global";
610
817
  const agents2 = await resolveAgents(options, scope);
611
818
  if (agents2.length === 0) return;
612
- const spinner = ora("Configuring GenRTL MCP...").start();
819
+ const spinner = ora(`Installing GenRTL ${mode.toUpperCase()} integration...`).start();
613
820
  const results = [];
614
821
  for (const agentName of agents2) {
615
822
  spinner.text = `Configuring ${getAgent(agentName).displayName}...`;
616
- results.push(await setupAgent(agentName, auth, scope));
823
+ results.push(await setupAgent(agentName, mode, auth, scope));
617
824
  }
618
825
  const failed = results.some(
619
- (result) => result.mcpStatus.startsWith("failed") || result.ruleStatus.startsWith("failed")
826
+ (result) => result.mcpStatus?.startsWith("failed") || result.ruleStatus.startsWith("failed") || result.skillStatus.startsWith("failed")
620
827
  );
621
828
  if (failed) {
622
829
  spinner.warn("GenRTL setup completed with errors");
@@ -627,13 +834,20 @@ async function setupCommand(options) {
627
834
  log.blank();
628
835
  for (const result of results) {
629
836
  log.plain(` ${pc3.bold(result.agent)}`);
630
- log.plain(` ${pc3.green("+")} MCP server ${result.mcpStatus}`);
631
- log.plain(` ${pc3.dim(result.mcpPath)}`);
837
+ if (result.mcpStatus && result.mcpPath) {
838
+ log.plain(` ${pc3.green("+")} MCP server ${result.mcpStatus}`);
839
+ log.plain(` ${pc3.dim(result.mcpPath)}`);
840
+ }
841
+ log.plain(` ${pc3.green("+")} Skill ${result.skillStatus}`);
842
+ log.plain(` ${pc3.dim(result.skillPath)}`);
632
843
  log.plain(` ${pc3.green("+")} Rule ${result.ruleStatus}`);
633
844
  log.plain(` ${pc3.dim(result.rulePath)}`);
634
845
  }
635
846
  log.blank();
636
- trackEvent("setup", { agents: agents2, scope, authMode: "api-key" });
847
+ if (mode === "cli" && !process.env.GRTL_API_KEY && !process.env.GENRTL_API_KEY) {
848
+ log.warn("Set GRTL_API_KEY or GENRTL_API_KEY in the coding agent's environment before use.");
849
+ }
850
+ trackEvent("setup", { agents: agents2, scope, mode, authMode: auth?.mode ?? "environment" });
637
851
  }
638
852
 
639
853
  // src/commands/knowledge.ts
@@ -644,14 +858,14 @@ import ora2 from "ora";
644
858
  // src/constants.ts
645
859
  import { readFileSync } from "fs";
646
860
  import { fileURLToPath } from "url";
647
- import { dirname as dirname3, join as join3 } from "path";
648
- var __dirname = dirname3(fileURLToPath(import.meta.url));
861
+ import { dirname as dirname5, join as join3 } from "path";
862
+ var __dirname = dirname5(fileURLToPath(import.meta.url));
649
863
  var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
650
864
  var VERSION = pkg.version;
651
865
  var NAME = pkg.name;
652
866
 
653
867
  // src/utils/knowledge-api.ts
654
- var baseUrl = "https://www.genrtl.com";
868
+ var baseUrl = "https://genrtl.com";
655
869
  function setBaseUrl(url) {
656
870
  baseUrl = url.replace(/\/+$/, "");
657
871
  }
@@ -659,7 +873,14 @@ function getMcpEndpoint() {
659
873
  return baseUrl.endsWith("/api/mcp") ? baseUrl : `${baseUrl}/api/mcp`;
660
874
  }
661
875
  function getApiKey() {
662
- return process.env.GRTL_API_KEY || process.env.GENRTL_API_KEY;
876
+ return [process.env.GRTL_API_KEY, process.env.GENRTL_API_KEY].map((value) => value?.trim()).find((value) => Boolean(value));
877
+ }
878
+ function validateApiKey(apiKey) {
879
+ if (!/^gtr_(?:live|test)_[A-Za-z0-9_-]{32,128}$/.test(apiKey)) {
880
+ throw new Error(
881
+ "Invalid GenRTL API key format. Use the full key shown once when it was created; it must start with gtr_live_ or gtr_test_."
882
+ );
883
+ }
663
884
  }
664
885
  function getMcpErrorMessage(content) {
665
886
  if (!Array.isArray(content)) return void 0;
@@ -668,11 +889,20 @@ function getMcpErrorMessage(content) {
668
889
  );
669
890
  return item?.text;
670
891
  }
892
+ function getStructuredMcpError(content) {
893
+ if (!content || typeof content !== "object") return void 0;
894
+ const errorContent = content;
895
+ return {
896
+ error: typeof errorContent.error === "string" ? errorContent.error : void 0,
897
+ code: typeof errorContent.code === "string" ? errorContent.code : void 0
898
+ };
899
+ }
671
900
  async function callGenrtlKnowledgeTool(toolName, input) {
672
901
  const apiKey = getApiKey();
673
902
  if (!apiKey) {
674
903
  throw new Error("Authentication required. Set GRTL_API_KEY or GENRTL_API_KEY.");
675
904
  }
905
+ validateApiKey(apiKey);
676
906
  const response = await fetch(getMcpEndpoint(), {
677
907
  method: "POST",
678
908
  headers: {
@@ -698,7 +928,9 @@ async function callGenrtlKnowledgeTool(toolName, input) {
698
928
  const result = payload?.result;
699
929
  if (!result) throw new Error("GenRTL MCP returned an empty result.");
700
930
  if (result.isError) {
701
- throw new Error(getMcpErrorMessage(result.content) || "GenRTL knowledge search failed.");
931
+ const structuredError = getStructuredMcpError(result.structuredContent);
932
+ const message = structuredError?.error || getMcpErrorMessage(result.content) || "GenRTL knowledge search failed.";
933
+ throw new Error(structuredError?.code ? `${message} (${structuredError.code})` : message);
702
934
  }
703
935
  if (!result.structuredContent) {
704
936
  throw new Error("GenRTL MCP response did not include structured knowledge results.");
@@ -851,8 +1083,8 @@ import pc5 from "picocolors";
851
1083
 
852
1084
  // src/utils/update-check.ts
853
1085
  import { homedir as homedir2 } from "os";
854
- import { dirname as dirname4, join as join4 } from "path";
855
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1086
+ import { dirname as dirname6, join as join4 } from "path";
1087
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
856
1088
  var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
857
1089
  var UPDATE_STATE_FILE = join4(homedir2(), ".genrtl", "cli-state.json");
858
1090
  function getStateFilePath(stateFile) {
@@ -868,8 +1100,8 @@ async function readUpdateState(stateFile) {
868
1100
  }
869
1101
  async function writeUpdateState(state, stateFile) {
870
1102
  const path = getStateFilePath(stateFile);
871
- await mkdir3(dirname4(path), { recursive: true });
872
- await writeFile3(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
1103
+ await mkdir4(dirname6(path), { recursive: true });
1104
+ await writeFile4(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
873
1105
  }
874
1106
  function compareVersions(a, b) {
875
1107
  const normalize = (version) => version.split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -1045,13 +1277,13 @@ function registerUpgradeCommand(program2) {
1045
1277
  });
1046
1278
  }
1047
1279
  function runCommand(command, args) {
1048
- return new Promise((resolve, reject) => {
1280
+ return new Promise((resolve3, reject) => {
1049
1281
  const child = spawn(command, args, {
1050
1282
  stdio: "inherit",
1051
1283
  shell: process.platform === "win32"
1052
1284
  });
1053
1285
  child.on("error", reject);
1054
- child.on("close", (code) => resolve(code));
1286
+ child.on("close", (code) => resolve3(code));
1055
1287
  });
1056
1288
  }
1057
1289
  async function runUpgradePlan(plan) {
@@ -1188,8 +1420,8 @@ program.name("grtl").description("GenRTL CLI - Search RTL engineering knowledge
1188
1420
  `
1189
1421
  Examples:
1190
1422
  ${brand.dim("# Configure GenRTL for your coding agent")}
1191
- ${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --cursor")}
1192
- ${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --codex --project")}
1423
+ ${brand.primary("npx @genrtl/grtl setup --cli --codex --project")}
1424
+ ${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --mcp --codex --project")}
1193
1425
 
1194
1426
  ${brand.dim("# Search the same four tools exposed by the GenRTL MCP server")}
1195
1427
  ${brand.primary('npx @genrtl/grtl knowledge-search "AXI stream backpressure design"')}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genrtl/grtl",
3
- "version": "0.1.0",
4
- "description": "CLI for GenRTL RTL engineering knowledge and MCP setup",
3
+ "version": "0.2.0",
4
+ "description": "CLI, MCP, and coding-agent Skills for GenRTL RTL engineering knowledge",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "grtl": "dist/index.js"
@@ -63,7 +63,7 @@
63
63
  "bugs": {
64
64
  "url": "https://github.com/xroting/grtl/issues"
65
65
  },
66
- "homepage": "https://www.genrtl.com",
66
+ "homepage": "https://genrtl.com",
67
67
  "publishConfig": {
68
68
  "access": "public"
69
69
  },