@blxzer/cursor-trellis 0.2.7 → 0.2.9

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 (88) hide show
  1. package/CHANGELOG.md +195 -133
  2. package/README.md +14 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +18 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +8 -1
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/update.d.ts.map +1 -1
  10. package/dist/commands/update.js +4 -0
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/validate-rules.d.ts +6 -0
  13. package/dist/commands/validate-rules.d.ts.map +1 -0
  14. package/dist/commands/validate-rules.js +33 -0
  15. package/dist/commands/validate-rules.js.map +1 -0
  16. package/dist/configurators/cursor2plus-local.d.ts.map +1 -1
  17. package/dist/configurators/cursor2plus-local.js +2 -1
  18. package/dist/configurators/cursor2plus-local.js.map +1 -1
  19. package/dist/migrations/manifests/0.2.2.json +8 -8
  20. package/dist/migrations/manifests/0.2.4.json +8 -8
  21. package/dist/migrations/manifests/0.2.5.json +8 -8
  22. package/dist/migrations/manifests/0.2.6.json +8 -8
  23. package/dist/migrations/manifests/0.2.7.json +8 -8
  24. package/dist/migrations/manifests/0.2.8.json +9 -0
  25. package/dist/migrations/manifests/0.2.9.json +9 -0
  26. package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +7 -8
  27. package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +46 -46
  28. package/dist/templates/common/bundled-skills/trellis-cursor2plus-setup/SKILL.md +2 -2
  29. package/dist/templates/common/commands/finish-work.md +12 -0
  30. package/dist/templates/common/skills/check.md +4 -0
  31. package/dist/templates/cursor/agents/trellis-check.md +32 -0
  32. package/dist/templates/cursor/agents/trellis-implement.md +11 -0
  33. package/dist/templates/cursor/agents/trellis-research.md +29 -0
  34. package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
  35. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
  36. package/dist/templates/cursor/fixtures/expected-rules.js +26 -0
  37. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
  38. package/dist/templates/cursor/rules/trellis-subagent-dispatch.mdc +34 -0
  39. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +42 -42
  40. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +12 -0
  41. package/dist/templates/trellis/config/execution-strategy-rules.json +30 -30
  42. package/dist/templates/trellis/local/README.md +61 -37
  43. package/dist/templates/trellis/local/index.d.ts +1 -0
  44. package/dist/templates/trellis/local/index.d.ts.map +1 -1
  45. package/dist/templates/trellis/local/index.js +1 -0
  46. package/dist/templates/trellis/local/index.js.map +1 -1
  47. package/dist/templates/trellis/local/patch_wpelc8.py +162 -38
  48. package/dist/templates/trellis/local/smoke.py +94 -0
  49. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
  50. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +16 -2
  51. package/dist/templates/trellis/scripts/common/execution_strategy.py +267 -267
  52. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +26 -7
  53. package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +18 -4
  54. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +33 -1
  55. package/dist/templates/trellis/scripts/task.py +12 -0
  56. package/dist/utils/codebase-retrieval-router.js +2 -2
  57. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  58. package/dist/utils/cursor-retrieval-env.d.ts +2 -0
  59. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
  60. package/dist/utils/cursor-retrieval-env.js +11 -2
  61. package/dist/utils/cursor-retrieval-env.js.map +1 -1
  62. package/dist/utils/mirror-check.d.ts +22 -0
  63. package/dist/utils/mirror-check.d.ts.map +1 -0
  64. package/dist/utils/mirror-check.js +90 -0
  65. package/dist/utils/mirror-check.js.map +1 -0
  66. package/dist/utils/normalize-text.d.ts +7 -0
  67. package/dist/utils/normalize-text.d.ts.map +1 -0
  68. package/dist/utils/normalize-text.js +16 -0
  69. package/dist/utils/normalize-text.js.map +1 -0
  70. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
  71. package/dist/utils/retrieval-agent-instructions.js +14 -7
  72. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  73. package/dist/utils/retrieval-execution-telemetry.d.ts +12 -3
  74. package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -1
  75. package/dist/utils/retrieval-execution-telemetry.js +37 -1
  76. package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
  77. package/dist/utils/retrieval-tool-classification.d.ts +2 -0
  78. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
  79. package/dist/utils/retrieval-tool-classification.js +13 -0
  80. package/dist/utils/retrieval-tool-classification.js.map +1 -1
  81. package/dist/utils/semantic-plan-gate.d.ts.map +1 -1
  82. package/dist/utils/semantic-plan-gate.js +11 -4
  83. package/dist/utils/semantic-plan-gate.js.map +1 -1
  84. package/dist/utils/validate-rules.d.ts +21 -0
  85. package/dist/utils/validate-rules.d.ts.map +1 -0
  86. package/dist/utils/validate-rules.js +88 -0
  87. package/dist/utils/validate-rules.js.map +1 -0
  88. package/package.json +4 -3
@@ -17,6 +17,7 @@ from trellis_task_models_config import (
17
17
  )
18
18
 
19
19
  MARKER = "/*TRELLIS_B2_WPeLc8*/"
20
+ INJECT_NEEDLE = "});return!UbAQWn||"
20
21
 
21
22
 
22
23
  def _repo_root(start: Path) -> Path:
@@ -111,28 +112,43 @@ def resolve_model_layers(bundle_dir: Path, config: dict, providers_path: Path) -
111
112
  return resolve_slots_to_slugs(merged, providers_path)
112
113
 
113
114
 
114
- def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
115
+ def configured_slot_map(bundle_dir: Path, config: dict) -> dict[str, list[str]]:
116
+ """Non-secret role → [primary, fallback, …] from JSON5 only (no providers.json read)."""
117
+ user_path = resolve_user_models_path(config)
118
+ project_path = resolve_project_models_path(bundle_dir, config)
119
+ user_slots = extract_raw_slots(load_task_models_document(user_path))
120
+ project_slots = (
121
+ extract_raw_slots(load_task_models_document(project_path))
122
+ if project_path
123
+ else {}
124
+ )
125
+ return _merge_slot_maps(user_slots, project_slots)
126
+
127
+
128
+ def _extension_candidates(config: dict) -> list[Path]:
115
129
  for key in ("extensionJs", "extension_js"):
116
130
  if isinstance(config.get(key), str) and config[key].strip():
117
- return Path(config[key].strip()).expanduser()
131
+ return [Path(config[key].strip()).expanduser()]
118
132
  env = os.environ.get("TRELLIS_CURSOR2PLUS_EXTENSION", "").strip()
119
133
  if env:
120
- return Path(env).expanduser()
134
+ return [Path(env).expanduser()]
121
135
  if sys.platform == "win32":
122
136
  local = os.environ.get("LOCALAPPDATA", "")
123
- candidates = [
137
+ return [
124
138
  Path(local) / "Programs" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
125
139
  Path(r"D:\cursor\resources\app\extensions\cursor2plus\dist\extension.js"),
126
140
  ]
127
- elif sys.platform == "darwin":
128
- candidates = [
141
+ if sys.platform == "darwin":
142
+ return [
129
143
  Path("/Applications/Cursor.app/Contents/Resources/app/extensions/cursor2plus/dist/extension.js"),
130
144
  ]
131
- else:
132
- candidates = [
133
- Path.home() / ".local" / "share" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
134
- ]
135
- for c in candidates:
145
+ return [
146
+ Path.home() / ".local" / "share" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
147
+ ]
148
+
149
+
150
+ def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
151
+ for c in _extension_candidates(config):
136
152
  if c.is_file():
137
153
  return c
138
154
  raise SystemExit(
@@ -140,6 +156,13 @@ def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
140
156
  )
141
157
 
142
158
 
159
+ def try_resolve_extension_js(bundle_dir: Path, config: dict) -> Path | None:
160
+ for c in _extension_candidates(config):
161
+ if c.is_file():
162
+ return c
163
+ return None
164
+
165
+
143
166
  def build_inject_block(model_map: dict[str, str]) -> str:
144
167
  if not model_map:
145
168
  raise SystemExit(
@@ -164,15 +187,27 @@ def find_wpelc8_body(source: str) -> tuple[int, int]:
164
187
  return start, end
165
188
 
166
189
 
190
+ def probe_wpelc8_compat(source: str) -> dict[str, str]:
191
+ """Non-exiting WPeLc8 / inject-anchor probe for --check-compat and smoke."""
192
+ start = source.find("function WPeLc8(")
193
+ if start < 0:
194
+ return {"wpelc8": "not_locatable", "inject_anchor": "unknown", "end_anchor": "unknown"}
195
+ end = source.find("async function*o4ZNpa", start)
196
+ if end < 0:
197
+ return {"wpelc8": "locatable", "inject_anchor": "unknown", "end_anchor": "missing"}
198
+ fn = source[start:end]
199
+ anchor = "present" if INJECT_NEEDLE in fn else "missing"
200
+ return {"wpelc8": "locatable", "inject_anchor": anchor, "end_anchor": "present"}
201
+
202
+
167
203
  def apply_patch(source: str, inject: str) -> str:
168
204
  start, end = find_wpelc8_body(source)
169
205
  fn = source[start:end]
170
206
  if MARKER in fn:
171
207
  fn = remove_patch_from_fn(fn)
172
- needle = "});return!UbAQWn||"
173
- if needle not in fn:
174
- raise SystemExit(f"inject anchor missing: {needle!r}")
175
- fn_new = fn.replace(needle, f"}});{inject}return!UbAQWn||", 1)
208
+ if INJECT_NEEDLE not in fn:
209
+ raise SystemExit(f"inject anchor missing: {INJECT_NEEDLE!r}")
210
+ fn_new = fn.replace(INJECT_NEEDLE, f"}});{inject}return!UbAQWn||", 1)
176
211
  return source[:start] + fn_new + source[end:]
177
212
 
178
213
 
@@ -240,14 +275,80 @@ def cmd_bootstrap(bundle_dir: Path) -> None:
240
275
  print("Next: in Cursor Agent, run skill trellis-cursor2plus-setup", file=sys.stderr)
241
276
 
242
277
 
278
+ def cmd_check_compat(bundle_dir: Path, config: dict) -> int:
279
+ extension = try_resolve_extension_js(bundle_dir, config)
280
+ result: dict[str, object] = {
281
+ "extension": str(extension) if extension else None,
282
+ "patch_marker_present": False,
283
+ "wpelc8": "unknown",
284
+ "inject_anchor": "unknown",
285
+ "end_anchor": "unknown",
286
+ }
287
+ if extension is None:
288
+ result["status"] = "unknown"
289
+ result["hint"] = (
290
+ "extension.js not found — set extensionJs in config.local.json or run --bootstrap; "
291
+ "manual verification required"
292
+ )
293
+ print(json.dumps(result, indent=2, sort_keys=True))
294
+ return 2
295
+
296
+ source = extension.read_text(encoding="utf-8", errors="ignore")
297
+ result["patch_marker_present"] = MARKER in source
298
+ probe = probe_wpelc8_compat(source)
299
+ result.update(probe)
300
+
301
+ wpelc8 = probe["wpelc8"]
302
+ anchor = probe["inject_anchor"]
303
+ end_anchor = probe.get("end_anchor", "unknown")
304
+
305
+ if wpelc8 == "not_locatable" or anchor == "missing":
306
+ result["status"] = "fail"
307
+ result["hint"] = (
308
+ "WPeLc8 or inject anchor not found after Cursor/Cursor++ upgrade — "
309
+ "re-run patch with --apply --approve or revert to inherit-parent"
310
+ )
311
+ elif wpelc8 == "unknown" or anchor == "unknown" or end_anchor == "unknown":
312
+ result["status"] = "unknown"
313
+ result["hint"] = "Locator uncertain — manually verify extension.js after any upgrade"
314
+ else:
315
+ result["status"] = "ok"
316
+ result["hint"] = "WPeLc8 and inject anchor locatable"
317
+
318
+ print(json.dumps(result, indent=2, sort_keys=True))
319
+ return 0 if result["status"] == "ok" else (1 if result["status"] == "fail" else 2)
320
+
321
+
243
322
  def main() -> None:
244
- ap = argparse.ArgumentParser(description="Trellis Cursor++ WPeLc8 model map")
245
- ap.add_argument("--revert", action="store_true")
246
- ap.add_argument("--dry-run", action="store_true")
247
- ap.add_argument("--bootstrap", action="store_true")
248
- ap.add_argument("--explain", action="store_true")
249
- ap.add_argument("--list-models", action="store_true")
250
- ap.add_argument("--print-map", action="store_true")
323
+ ap = argparse.ArgumentParser(
324
+ description="Trellis Cursor++ WPeLc8 model map (Method 2.5)",
325
+ epilog=(
326
+ "Read-only: --print-map, --check-compat. "
327
+ "Write: --apply --approve (or --revert). "
328
+ "Never runs in CI by default."
329
+ ),
330
+ )
331
+ ap.add_argument("--revert", action="store_true", help="Remove Trellis patch from extension.js")
332
+ ap.add_argument("--apply", action="store_true", help="Apply patch to extension.js")
333
+ ap.add_argument(
334
+ "--approve",
335
+ action="store_true",
336
+ help="Explicit operator approval (required for --apply writes)",
337
+ )
338
+ ap.add_argument(
339
+ "--dry-run",
340
+ action="store_true",
341
+ help="With --apply: validate patch without writing extension.js",
342
+ )
343
+ ap.add_argument("--bootstrap", action="store_true", help="Create config.local.json with extension path")
344
+ ap.add_argument("--explain", action="store_true", help="Explain trellis-task-models.json5 keys")
345
+ ap.add_argument("--list-models", action="store_true", help="List models from providers.json catalog")
346
+ ap.add_argument("--print-map", action="store_true", help="Dry-run: print resolved slug map (no file writes)")
347
+ ap.add_argument(
348
+ "--check-compat",
349
+ action="store_true",
350
+ help="Check WPeLc8 locatability in current extension.js (no file writes)",
351
+ )
251
352
  args = ap.parse_args()
252
353
 
253
354
  bundle_dir = Path(__file__).resolve().parent
@@ -259,39 +360,62 @@ def main() -> None:
259
360
  return
260
361
 
261
362
  config = _load_config(bundle_dir)
363
+
364
+ if args.check_compat:
365
+ raise SystemExit(cmd_check_compat(bundle_dir, config))
366
+
262
367
  providers_path = resolve_providers_json(config)
263
368
 
264
369
  if args.list_models:
265
370
  cmd_list_models(providers_path)
266
371
  return
267
372
 
268
- model_map, notes = resolve_model_layers(bundle_dir, config, providers_path)
269
- if notes:
270
- print("resolve:", file=sys.stderr)
271
- for line in notes:
272
- print(line, file=sys.stderr)
273
- if any("ERROR" in line for line in notes):
274
- raise SystemExit(1)
373
+ needs_map = args.print_map or args.apply or args.revert
374
+ model_map: dict[str, str] = {}
375
+ notes: list[str] = []
376
+ if needs_map and not args.revert:
377
+ model_map, notes = resolve_model_layers(bundle_dir, config, providers_path)
378
+ if notes:
379
+ print("resolve:", file=sys.stderr)
380
+ for line in notes:
381
+ print(line, file=sys.stderr)
382
+ if any("ERROR" in line for line in notes):
383
+ raise SystemExit(1)
275
384
 
276
385
  if args.print_map:
277
386
  print(json.dumps(model_map, indent=2, sort_keys=True))
278
387
  return
279
388
 
280
- extension = resolve_extension_js(bundle_dir, config)
281
- source = extension.read_text(encoding="utf-8", errors="ignore")
282
389
  if args.revert:
390
+ extension = resolve_extension_js(bundle_dir, config)
391
+ source = extension.read_text(encoding="utf-8", errors="ignore")
283
392
  new = remove_patch(source)
284
- else:
393
+ extension.write_text(new, encoding="utf-8", newline="\n")
394
+ print("wrote", extension)
395
+ return
396
+
397
+ if args.apply:
398
+ if not args.approve and not args.dry_run:
399
+ print(
400
+ "refused: --apply writes extension.js and requires explicit --approve "
401
+ "(use --dry-run to validate without writing)",
402
+ file=sys.stderr,
403
+ )
404
+ raise SystemExit(1)
405
+ extension = resolve_extension_js(bundle_dir, config)
406
+ source = extension.read_text(encoding="utf-8", errors="ignore")
285
407
  inject = build_inject_block(model_map)
286
408
  new = apply_patch(source, inject)
287
-
288
- if args.dry_run:
289
- print("ok", "revert" if args.revert else "patch", "delta", len(new) - len(source))
409
+ if args.dry_run:
410
+ print("ok patch delta", len(new) - len(source))
411
+ return
412
+ extension.write_text(new, encoding="utf-8", newline="\n")
413
+ print("wrote", extension)
290
414
  return
291
415
 
292
- extension.write_text(new, encoding="utf-8", newline="\n")
293
- print("wrote", extension)
416
+ ap.print_help()
417
+ raise SystemExit(2)
294
418
 
295
419
 
296
420
  if __name__ == "__main__":
297
- main()
421
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Cursor++ local bundle smoke check — whitelist metadata only, no secret reads."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from patch_wpelc8 import (
11
+ MARKER,
12
+ _load_config,
13
+ configured_slot_map,
14
+ probe_wpelc8_compat,
15
+ try_resolve_extension_js,
16
+ )
17
+
18
+ CAVEATS = [
19
+ "Cursor++ is third-party; reverse-engineering is version-bound",
20
+ "Never reads BYOK provider catalog file contents",
21
+ "Patch is operator-only; not part of CI or default Trellis workflow",
22
+ "Re-verify after any Cursor or Cursor++ upgrade",
23
+ ]
24
+
25
+
26
+ def detect_env() -> tuple[str, str | None]:
27
+ env_flag = os.environ.get("TRELLIS_CURSOR_BYOK", "").strip()
28
+ if env_flag == "1":
29
+ return "byok", "TRELLIS_CURSOR_BYOK=1"
30
+ if env_flag == "0":
31
+ return "native", "TRELLIS_CURSOR_BYOK=0"
32
+
33
+ routes = Path.home() / ".ccursor" / "routes.json"
34
+ if routes.is_file():
35
+ try:
36
+ data = json.loads(routes.read_text(encoding="utf-8"))
37
+ byok = data.get("byokMode")
38
+ if byok in (1, "1", True):
39
+ return "byok", "routes.json:byokMode"
40
+ if byok in (0, "0", False):
41
+ return "native", "routes.json:byokMode"
42
+ except (OSError, json.JSONDecodeError, TypeError):
43
+ pass
44
+ return "unknown", None
45
+
46
+
47
+ def detect_patch_status(bundle_dir: Path, config: dict) -> tuple[str, dict[str, object]]:
48
+ extension = try_resolve_extension_js(bundle_dir, config)
49
+ meta: dict[str, object] = {"extension": str(extension) if extension else None}
50
+ if extension is None:
51
+ return "not-applied", meta
52
+
53
+ source = extension.read_text(encoding="utf-8", errors="ignore")
54
+ marker_present = MARKER in source
55
+ meta["patch_marker_present"] = marker_present
56
+ probe = probe_wpelc8_compat(source)
57
+ meta.update(probe)
58
+
59
+ if not marker_present:
60
+ return "not-applied", meta
61
+
62
+ if probe["wpelc8"] == "not_locatable" or probe["inject_anchor"] == "missing":
63
+ return "stale", meta
64
+ if probe["wpelc8"] == "unknown" or probe["inject_anchor"] == "unknown":
65
+ return "stale", meta
66
+ return "applied", meta
67
+
68
+
69
+ def main() -> int:
70
+ bundle_dir = Path(__file__).resolve().parent
71
+ config = _load_config(bundle_dir)
72
+ env, env_source = detect_env()
73
+ patch_status, patch_meta = detect_patch_status(bundle_dir, config)
74
+ slots = configured_slot_map(bundle_dir, config)
75
+
76
+ report = {
77
+ "env": env,
78
+ "env_source": env_source,
79
+ "patch_status": patch_status,
80
+ "patch_meta": patch_meta,
81
+ "current_map": {
82
+ "configured_roles": {
83
+ role: {"candidates": names} for role, names in sorted(slots.items())
84
+ },
85
+ "note": "Role names from user JSON5 config only; slugs not resolved (no provider catalog read)",
86
+ },
87
+ "caveats": CAVEATS,
88
+ }
89
+ print(json.dumps(report, indent=2, sort_keys=True))
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__":
94
+ raise SystemExit(main())
@@ -14,8 +14,8 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  from .cursor_retrieval_env import (
17
- ENV_BYOK,
18
17
  detect_cursor_retrieval_env,
18
+ is_byok_conservative,
19
19
  semantic_route_spec,
20
20
  )
21
21
 
@@ -716,7 +716,7 @@ def _fallback_hints(
716
716
  "replacesRole": "semantic",
717
717
  })
718
718
  cenv = cursor_env or detect_cursor_retrieval_env()
719
- if cenv == ENV_BYOK:
719
+ if is_byok_conservative(cenv):
720
720
  hints.append({
721
721
  "when": "built-in @codebase / SemanticSearch not in agent tool list",
722
722
  "action": (
@@ -75,12 +75,26 @@ def is_byok(env: CursorRetrievalEnv | None = None) -> bool:
75
75
  return (env or detect_cursor_retrieval_env()) == ENV_BYOK
76
76
 
77
77
 
78
+ def is_byok_conservative(env: CursorRetrievalEnv | None = None) -> bool:
79
+ """Conservative semantic routing: BYOK plus unknown (no routes.json / ambiguous byokMode)."""
80
+ resolved = env or detect_cursor_retrieval_env()
81
+ return resolved == ENV_BYOK or resolved == ENV_UNKNOWN
82
+
83
+
78
84
  def semantic_route_spec(cursor_env: CursorRetrievalEnv) -> dict[str, object]:
79
85
  """platform-semantic route fields for conceptual / optional semantic slots."""
80
- if cursor_env == ENV_BYOK:
86
+ if is_byok_conservative(cursor_env):
87
+ unknown_caveat = (
88
+ " Unknown cursorEnv: conservative fast-context primary (same as BYOK)."
89
+ if cursor_env == ENV_UNKNOWN
90
+ else ""
91
+ )
81
92
  return {
82
93
  "commands": ["fast_context_search (fast-context MCP)"],
83
- "rationale_suffix": " Cursor++ BYOK: built-in semantic unavailable; fast-context MCP primary.",
94
+ "rationale_suffix": (
95
+ " Cursor++ BYOK: built-in semantic unavailable; fast-context MCP primary."
96
+ + unknown_caveat
97
+ ),
84
98
  "platformNative": False,
85
99
  "semanticBackend": "fast-context-mcp",
86
100
  }