@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.
- package/README.md +27 -64
- package/dist/index.js +465 -233
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
# grtl
|
|
1
|
+
# @genrtl/grtl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI and coding-agent integration for the GenRTL RTL engineering knowledge
|
|
4
|
+
service.
|
|
4
5
|
|
|
5
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
Install a Skill that tells the agent to call the `grtl` CLI:
|
|
55
22
|
|
|
56
23
|
```bash
|
|
57
|
-
grtl
|
|
58
|
-
|
|
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
|
-
|
|
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 --
|
|
71
|
-
grtl setup --
|
|
72
|
-
grtl setup --claude --opencode
|
|
31
|
+
grtl setup --mcp --codex
|
|
32
|
+
grtl setup --mcp --cursor --project
|
|
73
33
|
```
|
|
74
34
|
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
41
|
+
The hosted MCP endpoint is:
|
|
84
42
|
|
|
85
43
|
```text
|
|
86
|
-
https://
|
|
44
|
+
https://genrtl.com/api/mcp
|
|
87
45
|
```
|
|
88
46
|
|
|
89
|
-
|
|
47
|
+
## Knowledge Commands
|
|
90
48
|
|
|
91
49
|
```bash
|
|
92
|
-
grtl
|
|
93
|
-
grtl
|
|
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
|
|
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
|
|
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/
|
|
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://
|
|
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
|
|
123
|
-
|
|
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
|
-
|
|
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) => (
|
|
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) =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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", "
|
|
182
|
+
globalPaths: [join(homedir(), ".gemini", "config", "mcp_config.json")],
|
|
240
183
|
configKey: "mcpServers",
|
|
241
|
-
buildEntry: (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) => (
|
|
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(
|
|
228
|
+
async function pathExists(p) {
|
|
278
229
|
try {
|
|
279
|
-
await access(
|
|
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
|
|
290
|
-
const fullPath = scope === "global" ?
|
|
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
|
|
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 (
|
|
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(
|
|
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 {
|
|
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 {
|
|
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("
|
|
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 --
|
|
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(
|
|
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
|
|
542
|
-
await
|
|
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
|
|
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
|
|
562
|
-
await
|
|
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
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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("
|
|
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(
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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
|
|
648
|
-
var __dirname =
|
|
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://
|
|
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
|
|
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
|
-
|
|
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
|
|
855
|
-
import { mkdir as
|
|
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
|
|
872
|
-
await
|
|
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((
|
|
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) =>
|
|
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("
|
|
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.
|
|
4
|
-
"description": "CLI for GenRTL RTL engineering knowledge
|
|
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://
|
|
66
|
+
"homepage": "https://genrtl.com",
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|
|
69
69
|
},
|