@tencent-rtc/trtc-agent-skills 0.1.0 → 0.1.1
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/.cursor/rules/ui-mode.mdc +92 -0
- package/AGENTS.md +104 -0
- package/CLAUDE.md +126 -0
- package/CODEBUDDY.md +131 -0
- package/README.md +28 -42
- package/README.zh.md +24 -42
- package/ai-instructions/base.md +13 -0
- package/ai-instructions/ui-mode.md +86 -0
- package/bin/cli.js +428 -13
- package/hooks/cursor-adapter.py +315 -0
- package/hooks/hooks-cursor.json +39 -0
- package/hooks/hooks.json +67 -0
- package/package.json +7 -1
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
## When this rule applies
|
|
2
|
+
|
|
3
|
+
Only when `.trtc-session.yaml` at the repo root has `ui_mode: official-roomkit`.
|
|
4
|
+
If the file is missing, or `ui_mode` is unset / null / `headless`, this rule
|
|
5
|
+
does not apply — fall back to whatever the tool's default behavior is.
|
|
6
|
+
|
|
7
|
+
## Medical new-project shortcut
|
|
8
|
+
|
|
9
|
+
When the user request is for medical online consultation (for example
|
|
10
|
+
`1v1-video-consultation`, remote diagnosis, online medical consultation,
|
|
11
|
+
doctor-patient video visit) **and** the task is to generate a brand-new
|
|
12
|
+
project rather than integrate into an existing app, bypass the normal
|
|
13
|
+
integration workflow entirely. Do not generate Vue SFCs by hand, and do not run
|
|
14
|
+
any verifiers.
|
|
15
|
+
|
|
16
|
+
Instead, directly copy the bundled medical template project:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
skills/trtc/room-builder/templates/scenarios/medical-consultation/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
to the user's local target project directory, preserving the project exactly
|
|
23
|
+
as packaged. In the handoff or integration notes, tell the customer to use
|
|
24
|
+
`pnpm install` for dependencies and `pnpm dev` for local development. Do not
|
|
25
|
+
recommend `npm install` / `npm run dev`; this medical template starts much
|
|
26
|
+
slower with npm and can show a blank first screen for a while.
|
|
27
|
+
|
|
28
|
+
This shortcut applies only to brand-new medical consultation projects. Existing
|
|
29
|
+
project integration still follows the normal scenario / official-roomkit rules
|
|
30
|
+
below.
|
|
31
|
+
|
|
32
|
+
This is a terminal path. After choosing the bundled medical template, do not
|
|
33
|
+
enter the scenario/topic slice workflow, do not show a scenario capability table,
|
|
34
|
+
do not describe the project as a `general-conference` slice plan, and do not ask
|
|
35
|
+
about per-step auto-advance. Copy the template, mark the session completed, and
|
|
36
|
+
stop.
|
|
37
|
+
|
|
38
|
+
This is an internal execution rule, not user-facing copy. When explaining the
|
|
39
|
+
choice or reporting progress to the customer, do not mention "shortcut",
|
|
40
|
+
"bypass", "normal UI mode", "CLAUDE.md", or any internal rule file. Say that you
|
|
41
|
+
are creating a complete 1v1 video consultation project from the bundled medical
|
|
42
|
+
template, and explain only the outcome and next commands (`pnpm install`,
|
|
43
|
+
`pnpm dev`).
|
|
44
|
+
|
|
45
|
+
## Official RoomKit integration mode
|
|
46
|
+
|
|
47
|
+
When the customer asks to **integrate a conference / meeting into an existing
|
|
48
|
+
Vue 3 app** (for example "集成会议", "接入会议", "add video meeting to my app",
|
|
49
|
+
"official RoomKit", "TUIRoomKit"), use the official RoomKit integration path.
|
|
50
|
+
|
|
51
|
+
In this mode:
|
|
52
|
+
|
|
53
|
+
1. Integrate the official Web RoomKit package. For UI customization APIs,
|
|
54
|
+
verify that the resolved `@tencentcloud/roomkit-web-vue3` version is
|
|
55
|
+
`>=5.4.3` (installing `@tencentcloud/roomkit-web-vue3@5` is acceptable only
|
|
56
|
+
when the lockfile resolves to at least `5.4.3`), plus its documented peer
|
|
57
|
+
packages `tuikit-atomicx-vue3`,
|
|
58
|
+
`@tencentcloud/uikit-base-component-vue3`, and
|
|
59
|
+
`@tencentcloud/universal-api`.
|
|
60
|
+
2. Render the official components (`ConferenceMainView` for PC and
|
|
61
|
+
`ConferenceMainViewH5` for H5) inside `UIKitProvider`.
|
|
62
|
+
3. Use the official `conference` API for auth and room lifecycle:
|
|
63
|
+
`conference.login()`, `conference.setSelfInfo()`,
|
|
64
|
+
`conference.createAndJoinRoom()`, `conference.joinRoom()`,
|
|
65
|
+
`conference.leaveRoom()`, `conference.endRoom()`, and `RoomEvent`
|
|
66
|
+
listeners as appropriate for the customer's flow.
|
|
67
|
+
4. For UserSig, reuse the existing MCP / local-signing / backend-issued
|
|
68
|
+
credential flow. Do not generate `src/utils/usersig.ts`, do not expose
|
|
69
|
+
`SecretKey` in client code, and do not use `crypto-js`, `pako`,
|
|
70
|
+
`HmacSHA256`, or `tls-sig-api-v2` to sign UserSig in browser code.
|
|
71
|
+
5. For button / toolbar / pre-action UI adjustment, use only the official
|
|
72
|
+
customization APIs: `conference.setWidgetVisible()`,
|
|
73
|
+
`conference.registerWidget()`, and `conference.onWill()`.
|
|
74
|
+
6. Register `setWidgetVisible()`, `registerWidget()`, and `onWill()` after
|
|
75
|
+
`conference.login()` and before `conference.createAndJoinRoom()` /
|
|
76
|
+
`conference.joinRoom()` whenever possible, so built-in buttons do not
|
|
77
|
+
flicker and interceptors do not miss early clicks.
|
|
78
|
+
7. Use `conference.setFeatureConfig()` only for the feature configuration it
|
|
79
|
+
documents. In particular, configure `shareLink` immediately after
|
|
80
|
+
`conference.createAndJoinRoom()` / `conference.joinRoom()` succeeds, so
|
|
81
|
+
the final `roomId` is known.
|
|
82
|
+
8. Collect cleanup functions returned by `registerWidget()` and `onWill()`;
|
|
83
|
+
clean them on both `RoomEvent.ROOM_LEAVE` and `RoomEvent.ROOM_DISMISS`.
|
|
84
|
+
|
|
85
|
+
The acceptance check for this mode is that the app uses the official
|
|
86
|
+
package/components and official UI customization APIs.
|
package/bin/cli.js
CHANGED
|
@@ -48,6 +48,7 @@ const PKG_JSON = require(path.join(PKG_ROOT, "package.json"));
|
|
|
48
48
|
const PKG_VERSION = PKG_JSON.version || "0.0.0";
|
|
49
49
|
const SKILLS_SRC = path.join(PKG_ROOT, "skills");
|
|
50
50
|
const KB_SRC = path.join(PKG_ROOT, "knowledge-base");
|
|
51
|
+
const HOOKS_SRC = path.join(PKG_ROOT, "hooks");
|
|
51
52
|
|
|
52
53
|
// The 6 skills that make up the suite. Order is cosmetic; `trtc` is the entry.
|
|
53
54
|
const SKILL_NAMES = [
|
|
@@ -65,8 +66,11 @@ const IDE_TARGETS = {
|
|
|
65
66
|
claude: { skillsRoot: ".claude/skills", kind: "dir" },
|
|
66
67
|
cursor: { skillsRoot: ".cursor/skills", kind: "dir" },
|
|
67
68
|
codebuddy: { skillsRoot: ".codebuddy/skills", kind: "dir" },
|
|
68
|
-
// Codex
|
|
69
|
-
codex
|
|
69
|
+
// Codex looks for hooks at <repo>/.codex/hooks.json (per
|
|
70
|
+
// https://developers.openai.com/codex/hooks). We co-locate skills under
|
|
71
|
+
// .codex/ as well so the rewritten hook commands (which use absolute paths)
|
|
72
|
+
// point at the same root the hook config sits next to.
|
|
73
|
+
codex: { skillsRoot: ".codex/skills", kind: "dir" },
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
// MCP config locations per IDE.
|
|
@@ -84,6 +88,72 @@ const MCP_TARGETS = {
|
|
|
84
88
|
const MCP_SERVER_NAME = "tencent-rtc-skill-tool";
|
|
85
89
|
const MCP_SERVER_ENTRY = "@tencent-rtc/skill-tool@latest";
|
|
86
90
|
|
|
91
|
+
// Hooks distribution targets per IDE.
|
|
92
|
+
// claude / codebuddy / codex: hooks/ files copied to <root>/.{ide}/hooks/, and
|
|
93
|
+
// hooks/hooks.json is rewritten + merged into <root>/.{ide}/settings.json.
|
|
94
|
+
// The original hooks.json uses ${CLAUDE_PLUGIN_ROOT} / ${CODEBUDDY_PLUGIN_ROOT}
|
|
95
|
+
// placeholders that get expanded by the IDE in plugin mode; in npx mode we
|
|
96
|
+
// materialize them to absolute paths under the IDE's settings dir.
|
|
97
|
+
// cursor: hooks-cursor.json is rewritten + merged into ~/.cursor/hooks.json
|
|
98
|
+
// (USER-LEVEL — Cursor doesn't load project-level hooks). The
|
|
99
|
+
// cursor-adapter.py is copied to <root>/.cursor/hooks/ and its hardcoded
|
|
100
|
+
// $HOME/.cursor/plugins/local/... reference is rewritten to the actual path.
|
|
101
|
+
const HOOKS_TARGETS = {
|
|
102
|
+
claude: {
|
|
103
|
+
hooksDir: ".claude/hooks",
|
|
104
|
+
settingsFile: ".claude/settings.json",
|
|
105
|
+
sourceConfig: "hooks.json",
|
|
106
|
+
rootPlaceholder: "${CLAUDE_PLUGIN_ROOT}",
|
|
107
|
+
rootRewrite: ".claude",
|
|
108
|
+
fallbackPlaceholder: "${CODEBUDDY_PLUGIN_ROOT}",
|
|
109
|
+
},
|
|
110
|
+
codebuddy: {
|
|
111
|
+
hooksDir: ".codebuddy/hooks",
|
|
112
|
+
settingsFile: ".codebuddy/settings.json",
|
|
113
|
+
sourceConfig: "hooks.json",
|
|
114
|
+
rootPlaceholder: "${CODEBUDDY_PLUGIN_ROOT}",
|
|
115
|
+
rootRewrite: ".codebuddy",
|
|
116
|
+
fallbackPlaceholder: "${CLAUDE_PLUGIN_ROOT}",
|
|
117
|
+
},
|
|
118
|
+
codex: {
|
|
119
|
+
hooksDir: ".codex/hooks",
|
|
120
|
+
// Codex loads hooks from <repo>/.codex/hooks.json (or ~/.codex/hooks.json)
|
|
121
|
+
// — NOT from .agents/settings.json. See https://developers.openai.com/codex/hooks
|
|
122
|
+
settingsFile: ".codex/hooks.json",
|
|
123
|
+
sourceConfig: "hooks.json",
|
|
124
|
+
rootPlaceholder: "${CLAUDE_PLUGIN_ROOT}",
|
|
125
|
+
rootRewrite: ".codex",
|
|
126
|
+
fallbackPlaceholder: "${CODEBUDDY_PLUGIN_ROOT}",
|
|
127
|
+
},
|
|
128
|
+
cursor: {
|
|
129
|
+
hooksDir: ".cursor/hooks",
|
|
130
|
+
// ⚠ user-level — Cursor only loads ~/.cursor/hooks.json, not project-level.
|
|
131
|
+
settingsFile: path.join(os.homedir(), ".cursor", "hooks.json"),
|
|
132
|
+
sourceConfig: "hooks-cursor.json",
|
|
133
|
+
// The hardcoded path string we need to rewrite in hooks-cursor.json.
|
|
134
|
+
cursorAdapterPlaceholder: "$HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// AI instruction files distribution per IDE.
|
|
139
|
+
// - root-md : project-root markdown files (CLAUDE.md / AGENTS.md / CODEBUDDY.md).
|
|
140
|
+
// If the file already exists, our content is wrapped in HTML
|
|
141
|
+
// markers and injected/replaced inside the user's existing file.
|
|
142
|
+
// - cursor-rule : a Cursor MDC rule with `alwaysApply: true` frontmatter.
|
|
143
|
+
// Filename collision is virtually nil (users don't write a
|
|
144
|
+
// ui-mode.mdc themselves), so we just copy/overwrite.
|
|
145
|
+
const AI_INSTRUCTION_TARGETS = {
|
|
146
|
+
claude: { type: "root-md", filename: "CLAUDE.md" },
|
|
147
|
+
codex: { type: "root-md", filename: "AGENTS.md" },
|
|
148
|
+
codebuddy: { type: "root-md", filename: "CODEBUDDY.md" },
|
|
149
|
+
cursor: { type: "cursor-rule", filename: ".cursor/rules/ui-mode.mdc" },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Markers used to bracket our content inside user-owned root markdown files.
|
|
153
|
+
// Stable across versions so re-installs replace the prior block in place.
|
|
154
|
+
const MD_MARKER_BEGIN = "<!-- TRTC-AGENT-SKILLS:BEGIN -->";
|
|
155
|
+
const MD_MARKER_END = "<!-- TRTC-AGENT-SKILLS:END -->";
|
|
156
|
+
|
|
87
157
|
// Knowledge-base lives next to the skills root (sibling), because skills
|
|
88
158
|
// reference it via ${CLAUDE_PLUGIN_ROOT}/knowledge-base — we mirror that by
|
|
89
159
|
// placing knowledge-base/ as a sibling of the skills dir's parent. To keep it
|
|
@@ -151,6 +221,39 @@ function findProjectRoot(startCwd) {
|
|
|
151
221
|
return start;
|
|
152
222
|
}
|
|
153
223
|
|
|
224
|
+
// ── IDE auto-detection ────────────────────────────────────────────────────────
|
|
225
|
+
// When the user runs `npx ... add` without --ide, we auto-detect which IDEs
|
|
226
|
+
// they actually have installed by checking for their user-level config dirs.
|
|
227
|
+
// This way we don't pollute ~/.cursor/ for a user who only runs Claude Code.
|
|
228
|
+
//
|
|
229
|
+
// Detection markers per IDE — present means "this IDE is installed":
|
|
230
|
+
// claude : ~/.claude/ (created by Claude Code on first launch)
|
|
231
|
+
// cursor : ~/.cursor/ (created by Cursor on first launch)
|
|
232
|
+
// codebuddy : ~/.codebuddy/ (created by CodeBuddy on first launch)
|
|
233
|
+
// codex : ~/.codex/ (created by Codex CLI on first launch)
|
|
234
|
+
//
|
|
235
|
+
// If nothing matches, fall back to claude (the most common starting point).
|
|
236
|
+
// Adding a new IDE later means: one new entry here + entries in the existing
|
|
237
|
+
// IDE_TARGETS / HOOKS_TARGETS / AI_INSTRUCTION_TARGETS / MCP_TARGETS maps.
|
|
238
|
+
const IDE_DETECTION_MARKERS = {
|
|
239
|
+
claude: [".claude"],
|
|
240
|
+
cursor: [".cursor"],
|
|
241
|
+
codebuddy: [".codebuddy"],
|
|
242
|
+
codex: [".codex"],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function detectInstalledIDEs() {
|
|
246
|
+
const home = os.homedir();
|
|
247
|
+
const detected = [];
|
|
248
|
+
for (const ide of Object.keys(IDE_TARGETS)) {
|
|
249
|
+
const markers = IDE_DETECTION_MARKERS[ide] || [];
|
|
250
|
+
if (markers.some(m => fs.existsSync(path.join(home, m)))) {
|
|
251
|
+
detected.push(ide);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return detected.length > 0 ? detected : ["claude"];
|
|
255
|
+
}
|
|
256
|
+
|
|
154
257
|
// ── argv parsing ──────────────────────────────────────────────────────────────
|
|
155
258
|
function getFlag(args, name) {
|
|
156
259
|
const i = args.indexOf(name);
|
|
@@ -163,16 +266,23 @@ function printHelp() {
|
|
|
163
266
|
${c.bold("@tencent-rtc/trtc-agent-skills")} — Install TRTC AI Integration skills + MCP
|
|
164
267
|
|
|
165
268
|
${c.bold("Usage:")}
|
|
166
|
-
${c.cyan("npx @tencent-rtc/trtc-agent-skills add")}
|
|
167
|
-
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --ide <name>")}
|
|
269
|
+
${c.cyan("npx @tencent-rtc/trtc-agent-skills add")} Auto-detect installed IDEs and install for each
|
|
270
|
+
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --ide <name>")} Install only for that IDE: claude / cursor / codebuddy / codex
|
|
271
|
+
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --ide all")} Install for every supported IDE
|
|
168
272
|
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --clean")} Wipe existing trtc* skill dirs first
|
|
169
273
|
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --no-report")} Skip anonymous install reporting
|
|
170
274
|
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --list")} List skills shipped in this package
|
|
171
275
|
${c.cyan("npx @tencent-rtc/trtc-agent-skills add --help")} Show this help
|
|
172
276
|
|
|
277
|
+
${c.bold("Default behavior (no --ide):")}
|
|
278
|
+
${c.gray("Detects which IDEs are installed by checking ~/.{claude,cursor,codebuddy,codex}/")}
|
|
279
|
+
${c.gray("and installs for each one found. Falls back to claude if none detected.")}
|
|
280
|
+
|
|
173
281
|
${c.bold("Installs:")}
|
|
174
|
-
${c.dim("Skills :")} ${c.gray("<projectRoot>/.
|
|
282
|
+
${c.dim("Skills :")} ${c.gray("<projectRoot>/.{ide}/skills/")}
|
|
175
283
|
${c.dim("KB :")} ${c.gray("alongside the skills root as knowledge-base/")}
|
|
284
|
+
${c.dim("Hooks :")} ${c.gray("<projectRoot>/.{ide}/hooks/ + settings file with hook events wired")}
|
|
285
|
+
${c.dim("Rules :")} ${c.gray("CLAUDE.md / AGENTS.md / CODEBUDDY.md (marker-merged)")}
|
|
176
286
|
${c.dim("MCP :")} ${c.gray("tencent-rtc-skill-tool → IDE mcp config (npx @tencent-rtc/skill-tool@latest)")}
|
|
177
287
|
|
|
178
288
|
${c.dim("Skills are copied as sibling dirs so relative routing (../trtc-onboarding) keeps working.")}
|
|
@@ -201,9 +311,78 @@ function cleanSkills(skillsRootAbs) {
|
|
|
201
311
|
// also wipe a co-located knowledge-base copy if present
|
|
202
312
|
const kb = path.join(path.dirname(skillsRootAbs), "knowledge-base");
|
|
203
313
|
if (fs.existsSync(kb)) { rmrf(kb); }
|
|
314
|
+
// also wipe a co-located hooks/ copy if present (npx-mode hook scripts)
|
|
315
|
+
const hooks = path.join(path.dirname(skillsRootAbs), "hooks");
|
|
316
|
+
if (fs.existsSync(hooks)) { rmrf(hooks); }
|
|
204
317
|
return wiped;
|
|
205
318
|
}
|
|
206
319
|
|
|
320
|
+
// Strip our markered block from a root markdown file. If the file becomes
|
|
321
|
+
// empty after removal, delete it; otherwise leave the user's own content.
|
|
322
|
+
function cleanAiInstructions(ideList, resolvedRoot) {
|
|
323
|
+
for (const ide of ideList) {
|
|
324
|
+
const target = AI_INSTRUCTION_TARGETS[ide];
|
|
325
|
+
if (!target) continue;
|
|
326
|
+
const destAbs = path.join(resolvedRoot, target.filename);
|
|
327
|
+
if (!fs.existsSync(destAbs)) continue;
|
|
328
|
+
|
|
329
|
+
if (target.type === "cursor-rule") {
|
|
330
|
+
// .cursor/rules/ui-mode.mdc was installed verbatim by us; safe to remove.
|
|
331
|
+
rmrf(destAbs);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (target.type === "root-md") {
|
|
335
|
+
let content = fs.readFileSync(destAbs, "utf8");
|
|
336
|
+
const re = new RegExp(`\\n*${escapeRegex(MD_MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(MD_MARKER_END)}\\n?`, "g");
|
|
337
|
+
const stripped = content.replace(re, "").trimEnd();
|
|
338
|
+
if (!stripped) {
|
|
339
|
+
// The file existed only because we created it. Remove entirely.
|
|
340
|
+
rmrf(destAbs);
|
|
341
|
+
} else if (stripped !== content.trimEnd()) {
|
|
342
|
+
fs.writeFileSync(destAbs, stripped + "\n", "utf8");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Strip our hook entries from each IDE's settings.json. We tag entries with
|
|
349
|
+
// __trtc_agent_skills__ so we can filter precisely without disturbing the
|
|
350
|
+
// user's own hook entries (relevant for cursor's user-level hooks.json).
|
|
351
|
+
function cleanHooksSettings(ideList, resolvedRoot) {
|
|
352
|
+
for (const ide of ideList) {
|
|
353
|
+
const target = HOOKS_TARGETS[ide];
|
|
354
|
+
if (!target) continue;
|
|
355
|
+
|
|
356
|
+
const settingsPath = path.isAbsolute(target.settingsFile)
|
|
357
|
+
? target.settingsFile
|
|
358
|
+
: path.join(resolvedRoot, target.settingsFile);
|
|
359
|
+
if (!fs.existsSync(settingsPath)) continue;
|
|
360
|
+
|
|
361
|
+
let settings;
|
|
362
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
|
|
363
|
+
catch { continue; }
|
|
364
|
+
if (!settings || typeof settings !== "object") continue;
|
|
365
|
+
|
|
366
|
+
if (settings.__trtc_agent_skills__) delete settings.__trtc_agent_skills__;
|
|
367
|
+
|
|
368
|
+
if (settings.hooks && typeof settings.hooks === "object") {
|
|
369
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
370
|
+
const val = settings.hooks[event];
|
|
371
|
+
if (Array.isArray(val)) {
|
|
372
|
+
settings.hooks[event] = val.filter(e => !(e && typeof e === "object" && e.__trtc_agent_skills__));
|
|
373
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
374
|
+
} else {
|
|
375
|
+
// Non-array (claude/codebuddy style) — we own the whole event under our install.
|
|
376
|
+
delete settings.hooks[event];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
207
386
|
function installSkills(skillsRootAbs) {
|
|
208
387
|
ensureDir(skillsRootAbs);
|
|
209
388
|
for (const name of SKILL_NAMES) {
|
|
@@ -225,6 +404,207 @@ function copyKnowledgeBase(skillsRootAbs) {
|
|
|
225
404
|
return dest;
|
|
226
405
|
}
|
|
227
406
|
|
|
407
|
+
// ── hooks installation ────────────────────────────────────────────────────────
|
|
408
|
+
// In plugin mode the IDE expands ${CLAUDE_PLUGIN_ROOT} / ${CODEBUDDY_PLUGIN_ROOT}
|
|
409
|
+
// to the plugin install root. In npx mode there's no plugin root, so we
|
|
410
|
+
// materialize those placeholders to absolute paths pointing at the IDE's
|
|
411
|
+
// settings dir (where we put .{ide}/skills/, .{ide}/hooks/, etc).
|
|
412
|
+
function rewriteHooksContent(content, target, ideAbsRoot) {
|
|
413
|
+
let out = content;
|
|
414
|
+
if (target.rootPlaceholder) {
|
|
415
|
+
// Replace BOTH ${CLAUDE_PLUGIN_ROOT} and ${CODEBUDDY_PLUGIN_ROOT} — the
|
|
416
|
+
// bundled hooks.json uses `${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}`
|
|
417
|
+
// shell fallback, but in JSON-merged form (settings.json hooks field) the
|
|
418
|
+
// shell expansion still applies because hook commands run in a shell. We
|
|
419
|
+
// pre-resolve both for clarity and so plain-string consumers also work.
|
|
420
|
+
const placeholders = [target.rootPlaceholder, target.fallbackPlaceholder].filter(Boolean);
|
|
421
|
+
for (const ph of placeholders) {
|
|
422
|
+
out = out.split(ph).join(ideAbsRoot);
|
|
423
|
+
}
|
|
424
|
+
// The bash `${VAR:-${OTHER}}` form leaves a literal `:-` between two
|
|
425
|
+
// already-replaced absolute paths, which won't run. Simplify it: collapse
|
|
426
|
+
// `<abs>:-<abs>` (or any duplicated form) back to a single `<abs>`.
|
|
427
|
+
out = out.replace(/\$\{[A-Z_]+:-[^}]+\}/g, ideAbsRoot);
|
|
428
|
+
}
|
|
429
|
+
if (target.cursorAdapterPlaceholder) {
|
|
430
|
+
// hooks-cursor.json hardcodes $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py
|
|
431
|
+
// — rewrite to the project-local copy we just installed. The placeholder
|
|
432
|
+
// sits inside a JSON string for a shell command (`python3 <path> arg`).
|
|
433
|
+
// We need the resulting JSON string to evaluate to a shell-quoted path so
|
|
434
|
+
// project paths with spaces don't break shell parsing — that means
|
|
435
|
+
// emitting `\"<abs>\"` (JSON-escaped quotes) into the string.
|
|
436
|
+
const cursorAdapterAbs = path.join(ideAbsRoot, "hooks", "cursor-adapter.py");
|
|
437
|
+
const replacement = `\\"${cursorAdapterAbs}\\"`;
|
|
438
|
+
out = out.split(target.cursorAdapterPlaceholder).join(replacement);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Copy the hooks/ source directory into <root>/.{ide}/hooks/ so the dispatched
|
|
444
|
+
// scripts (cursor-adapter.py + the underlying guardrail scripts referenced by
|
|
445
|
+
// hooks.json) sit next to the IDE's skills/.
|
|
446
|
+
function copyHooksDir(target, resolvedRoot) {
|
|
447
|
+
const dest = path.join(resolvedRoot, target.hooksDir);
|
|
448
|
+
rmrf(dest);
|
|
449
|
+
copyRecursive(HOOKS_SRC, dest);
|
|
450
|
+
return dest;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Merge the rewritten hook config into the IDE's settings file. The settings
|
|
454
|
+
// file may already contain unrelated user state (permissions, MCP servers,
|
|
455
|
+
// other hooks); we only own the `hooks` key. For Cursor's user-level
|
|
456
|
+
// ~/.cursor/hooks.json we merge per-event arrays so a previously-installed
|
|
457
|
+
// project's adapter path gets replaced by ours but the user's own hook
|
|
458
|
+
// entries (if any) are preserved.
|
|
459
|
+
function mergeHooksConfig(target, resolvedRoot, ideAbsRoot) {
|
|
460
|
+
const srcPath = path.join(HOOKS_SRC, target.sourceConfig);
|
|
461
|
+
if (!fs.existsSync(srcPath)) return null;
|
|
462
|
+
|
|
463
|
+
const rawSrc = fs.readFileSync(srcPath, "utf8");
|
|
464
|
+
const rewritten = rewriteHooksContent(rawSrc, target, ideAbsRoot);
|
|
465
|
+
let parsed;
|
|
466
|
+
try { parsed = JSON.parse(rewritten); }
|
|
467
|
+
catch (err) {
|
|
468
|
+
console.error(c.red(` ✗ failed to parse rewritten ${target.sourceConfig}: ${err.message}`));
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const settingsPath = path.isAbsolute(target.settingsFile)
|
|
473
|
+
? target.settingsFile
|
|
474
|
+
: path.join(resolvedRoot, target.settingsFile);
|
|
475
|
+
ensureDir(path.dirname(settingsPath));
|
|
476
|
+
|
|
477
|
+
let existing = {};
|
|
478
|
+
if (fs.existsSync(settingsPath)) {
|
|
479
|
+
try { existing = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
|
|
480
|
+
catch { existing = {}; }
|
|
481
|
+
}
|
|
482
|
+
if (!existing || typeof existing !== "object") existing = {};
|
|
483
|
+
|
|
484
|
+
// The hooks payload sits under `hooks` (claude/codebuddy/cursor/codex all
|
|
485
|
+
// use this key). For Cursor we additionally track our injected entries so we
|
|
486
|
+
// can later remove only ours on uninstall.
|
|
487
|
+
const incomingHooks = parsed.hooks || {};
|
|
488
|
+
if (!existing.hooks || typeof existing.hooks !== "object") existing.hooks = {};
|
|
489
|
+
|
|
490
|
+
// Marker used inside user-level cursor hooks.json to identify our entries
|
|
491
|
+
// when multiple projects install. Tagged on each individual hook entry so
|
|
492
|
+
// a future uninstall can filter precisely.
|
|
493
|
+
const tagged = (entry) => {
|
|
494
|
+
if (entry && typeof entry === "object") {
|
|
495
|
+
return Object.assign({}, entry, { __trtc_agent_skills__: true });
|
|
496
|
+
}
|
|
497
|
+
return entry;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
for (const [eventName, eventValue] of Object.entries(incomingHooks)) {
|
|
501
|
+
if (Array.isArray(eventValue)) {
|
|
502
|
+
// Cursor format: hooks.<event> = [{command: ...}, ...]
|
|
503
|
+
const stripped = (existing.hooks[eventName] || [])
|
|
504
|
+
.filter(e => !(e && typeof e === "object" && e.__trtc_agent_skills__));
|
|
505
|
+
existing.hooks[eventName] = stripped.concat(eventValue.map(tagged));
|
|
506
|
+
} else if (Array.isArray(existing.hooks[eventName])) {
|
|
507
|
+
// existing is array (cursor-style), incoming is non-array (claude-style):
|
|
508
|
+
// overwrite — this combination shouldn't happen in practice.
|
|
509
|
+
existing.hooks[eventName] = eventValue;
|
|
510
|
+
} else {
|
|
511
|
+
// Claude/Codebuddy format: hooks.<event> = [{matcher, hooks: [...]}, ...]
|
|
512
|
+
// The bundled hooks.json IS the only source for this key; just replace.
|
|
513
|
+
existing.hooks[eventName] = eventValue;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Top-level marker so a future uninstall can detect our presence quickly.
|
|
518
|
+
existing.__trtc_agent_skills__ = {
|
|
519
|
+
version: PKG_VERSION,
|
|
520
|
+
hookEvents: Object.keys(incomingHooks),
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Preserve / propagate top-level keys that the IDE expects (e.g. cursor
|
|
524
|
+
// requires `"version": 1` at the root of ~/.cursor/hooks.json or it rejects
|
|
525
|
+
// the file with "Config version must be a number"). Only copy keys we don't
|
|
526
|
+
// already own (hooks, __trtc_agent_skills__) to avoid clobbering the user's
|
|
527
|
+
// unrelated state.
|
|
528
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
529
|
+
if (key === "hooks" || key === "__trtc_agent_skills__") continue;
|
|
530
|
+
if (existing[key] === undefined) existing[key] = val;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
534
|
+
return { settingsPath, eventCount: Object.keys(incomingHooks).length };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function installHooks(ideList, resolvedRoot) {
|
|
538
|
+
for (const ide of ideList) {
|
|
539
|
+
const target = HOOKS_TARGETS[ide];
|
|
540
|
+
if (!target) continue;
|
|
541
|
+
|
|
542
|
+
const ideAbsRoot = path.join(resolvedRoot, path.dirname(target.hooksDir));
|
|
543
|
+
const hooksDest = copyHooksDir(target, resolvedRoot);
|
|
544
|
+
console.log(c.green(" ✓ ") + `${ide} hooks → ${hooksDest}/`);
|
|
545
|
+
|
|
546
|
+
const merged = mergeHooksConfig(target, resolvedRoot, ideAbsRoot);
|
|
547
|
+
if (merged) {
|
|
548
|
+
const isUserLevel = path.isAbsolute(target.settingsFile);
|
|
549
|
+
const prefix = isUserLevel ? c.yellow(" ⚠ ") : c.green(" ✓ ");
|
|
550
|
+
const note = isUserLevel ? c.dim(" (user-level — affects all cursor projects)") : "";
|
|
551
|
+
console.log(`${prefix}${ide} hooks settings → ${merged.settingsPath} ${c.dim(`(${merged.eventCount} events)`)}${note}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── AI instruction files installation ─────────────────────────────────────────
|
|
557
|
+
function escapeRegex(s) {
|
|
558
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function injectMarkered(srcAbs, destAbs) {
|
|
562
|
+
const trtcContent = fs.readFileSync(srcAbs, "utf8").trimEnd();
|
|
563
|
+
const block = `${MD_MARKER_BEGIN}\n${trtcContent}\n${MD_MARKER_END}\n`;
|
|
564
|
+
|
|
565
|
+
ensureDir(path.dirname(destAbs));
|
|
566
|
+
if (!fs.existsSync(destAbs)) {
|
|
567
|
+
fs.writeFileSync(destAbs, block, "utf8");
|
|
568
|
+
return "new";
|
|
569
|
+
}
|
|
570
|
+
let existing = fs.readFileSync(destAbs, "utf8");
|
|
571
|
+
const re = new RegExp(`${escapeRegex(MD_MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(MD_MARKER_END)}\\n?`, "g");
|
|
572
|
+
if (re.test(existing)) {
|
|
573
|
+
existing = existing.replace(re, block);
|
|
574
|
+
fs.writeFileSync(destAbs, existing, "utf8");
|
|
575
|
+
return "replaced";
|
|
576
|
+
}
|
|
577
|
+
existing = existing.trimEnd() + "\n\n" + block;
|
|
578
|
+
fs.writeFileSync(destAbs, existing, "utf8");
|
|
579
|
+
return "appended";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function installAiInstructions(ideList, resolvedRoot) {
|
|
583
|
+
for (const ide of ideList) {
|
|
584
|
+
const target = AI_INSTRUCTION_TARGETS[ide];
|
|
585
|
+
if (!target) continue;
|
|
586
|
+
|
|
587
|
+
const srcAbs = path.join(PKG_ROOT, target.filename);
|
|
588
|
+
const destAbs = path.join(resolvedRoot, target.filename);
|
|
589
|
+
if (!fs.existsSync(srcAbs)) {
|
|
590
|
+
console.log(c.dim(` ✓ ${ide} instructions skipped (source missing)`));
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (target.type === "cursor-rule") {
|
|
595
|
+
ensureDir(path.dirname(destAbs));
|
|
596
|
+
fs.copyFileSync(srcAbs, destAbs);
|
|
597
|
+
console.log(c.green(" ✓ ") + `${ide} rule → ${destAbs}`);
|
|
598
|
+
} else if (target.type === "root-md") {
|
|
599
|
+
const action = injectMarkered(srcAbs, destAbs);
|
|
600
|
+
const verb = action === "new" ? "created"
|
|
601
|
+
: action === "replaced" ? "updated marker block"
|
|
602
|
+
: "appended marker block";
|
|
603
|
+
console.log(c.green(" ✓ ") + `${ide} instructions → ${destAbs} ${c.dim(`(${verb})`)}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
228
608
|
// ── MCP installation ──────────────────────────────────────────────────────────
|
|
229
609
|
function installMcp(ideList, resolvedRoot) {
|
|
230
610
|
const serverEntry = {
|
|
@@ -373,9 +753,24 @@ function main() {
|
|
|
373
753
|
|
|
374
754
|
const isClean = args.includes("--clean");
|
|
375
755
|
const noReport = args.includes("--no-report");
|
|
376
|
-
const ideArg = getFlag(args, "--ide")
|
|
377
|
-
|
|
378
|
-
|
|
756
|
+
const ideArg = getFlag(args, "--ide");
|
|
757
|
+
|
|
758
|
+
// Resolve ideList:
|
|
759
|
+
// no --ide → auto-detect installed IDEs (default behavior)
|
|
760
|
+
// --ide all → install for every supported IDE
|
|
761
|
+
// --ide <name> → install for that specific IDE only
|
|
762
|
+
let ideList;
|
|
763
|
+
let ideListSource; // for the CLI hint
|
|
764
|
+
if (!ideArg) {
|
|
765
|
+
ideList = detectInstalledIDEs();
|
|
766
|
+
ideListSource = "auto-detected";
|
|
767
|
+
} else if (ideArg === "all") {
|
|
768
|
+
ideList = Object.keys(IDE_TARGETS);
|
|
769
|
+
ideListSource = "all";
|
|
770
|
+
} else {
|
|
771
|
+
ideList = [ideArg];
|
|
772
|
+
ideListSource = "explicit";
|
|
773
|
+
}
|
|
379
774
|
for (const ide of ideList) {
|
|
380
775
|
if (!IDE_TARGETS[ide]) {
|
|
381
776
|
console.error(c.red(`\n ✗ Unknown IDE: ${ide}. Valid: ${Object.keys(IDE_TARGETS).join(", ")}, all\n`));
|
|
@@ -391,10 +786,21 @@ function main() {
|
|
|
391
786
|
console.log(`\n ${c.bold(c.cyan("@tencent-rtc/trtc-agent-skills"))} ${c.dim("v" + PKG_VERSION)}`);
|
|
392
787
|
console.log(` ${c.gray("cwd : " + cwd)}`);
|
|
393
788
|
console.log(` ${c.gray("projectRoot : " + resolvedRoot)}`);
|
|
394
|
-
|
|
789
|
+
const ideHint = ideListSource === "auto-detected"
|
|
790
|
+
? c.dim(" (auto-detected; pass --ide all or --ide <name> to override)")
|
|
791
|
+
: ideListSource === "all"
|
|
792
|
+
? c.dim(" (--ide all)")
|
|
793
|
+
: "";
|
|
794
|
+
console.log(` ${c.gray("IDE(s) : " + ideList.join(", "))}${ideHint}`);
|
|
395
795
|
console.log("");
|
|
396
796
|
|
|
397
797
|
// 1. Install skill dirs (+ co-located knowledge-base) for each IDE.
|
|
798
|
+
if (isClean) {
|
|
799
|
+
// Clean settings hooks + AI instruction markers BEFORE we wipe the IDE
|
|
800
|
+
// dirs, so we can read the existing settings.json files in place.
|
|
801
|
+
cleanHooksSettings(ideList, resolvedRoot);
|
|
802
|
+
cleanAiInstructions(ideList, resolvedRoot);
|
|
803
|
+
}
|
|
398
804
|
for (const ide of ideList) {
|
|
399
805
|
const target = IDE_TARGETS[ide];
|
|
400
806
|
const skillsRootAbs = path.join(resolvedRoot, target.skillsRoot);
|
|
@@ -412,16 +818,25 @@ function main() {
|
|
|
412
818
|
console.log(c.green(" ✓ ") + "knowledge-base/ " + c.dim("→ " + kbDest));
|
|
413
819
|
}
|
|
414
820
|
|
|
415
|
-
// 2. Install
|
|
821
|
+
// 2. Install hooks (per-IDE: copy hooks dir + merge settings.json hooks).
|
|
822
|
+
console.log(`\n ${c.bold("HOOKS")}`);
|
|
823
|
+
installHooks(ideList, resolvedRoot);
|
|
824
|
+
|
|
825
|
+
// 3. Install AI instruction files (CLAUDE.md / AGENTS.md / CODEBUDDY.md /
|
|
826
|
+
// .cursor/rules/ui-mode.mdc) so the agent has routing rules.
|
|
827
|
+
console.log(`\n ${c.bold("AI INSTRUCTIONS")}`);
|
|
828
|
+
installAiInstructions(ideList, resolvedRoot);
|
|
829
|
+
|
|
830
|
+
// 4. Install MCP server config + permissions.
|
|
416
831
|
console.log(`\n ${c.bold("MCP")}`);
|
|
417
832
|
installMcp(ideList, resolvedRoot);
|
|
418
833
|
installClaudePermissions(ideList, resolvedRoot);
|
|
419
834
|
installCursorPermissions(ideList, resolvedRoot);
|
|
420
835
|
|
|
421
|
-
//
|
|
422
|
-
if (!noReport) reportInstall({ ide: ideArg });
|
|
836
|
+
// 5. Anonymous install reporting (fire-and-forget; opt out via --no-report).
|
|
837
|
+
if (!noReport) reportInstall({ ide: ideArg || ideListSource });
|
|
423
838
|
|
|
424
|
-
//
|
|
839
|
+
// 6. Done.
|
|
425
840
|
console.log(`\n ${c.bold("Done.")} ${c.dim("Just describe what you want to build in your IDE — the skill activates automatically.")}\n`);
|
|
426
841
|
}
|
|
427
842
|
|