@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.
Files changed (38) hide show
  1. package/.cursor/rules/ui-mode.mdc +92 -0
  2. package/AGENTS.md +104 -0
  3. package/CLAUDE.md +126 -0
  4. package/CODEBUDDY.md +131 -0
  5. package/README.md +28 -42
  6. package/README.zh.md +24 -42
  7. package/ai-instructions/base.md +13 -0
  8. package/ai-instructions/ui-mode.md +86 -0
  9. package/bin/cli.js +428 -13
  10. package/hooks/cursor-adapter.py +315 -0
  11. package/hooks/hooks-cursor.json +39 -0
  12. package/hooks/hooks.json +67 -0
  13. package/package.json +7 -1
  14. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  16. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  17. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  18. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  19. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  20. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  21. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  22. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  23. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  24. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  25. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  26. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  27. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  28. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  29. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  30. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  31. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  32. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  33. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  34. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  36. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  38. 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 reads project-root .agents/skills/. Same dir-per-skill layout works.
69
- codex: { skillsRoot: ".agents/skills", kind: "dir" },
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")} Install (default IDE: claude)
167
- ${c.cyan("npx @tencent-rtc/trtc-agent-skills add --ide <name>")} One of: claude / cursor / codebuddy / codex / all
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>/.claude/skills/ (or .cursor/, .codebuddy/, .agents/)")}
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") || "claude";
377
-
378
- const ideList = ideArg === "all" ? Object.keys(IDE_TARGETS) : [ideArg];
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
- console.log(` ${c.gray("IDE(s) : " + ideList.join(", "))}`);
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 MCP server config + permissions.
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
- // 3. Anonymous install reporting (fire-and-forget; opt out via --no-report).
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
- // 4. Done.
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