@blxzer/cursor-trellis 0.2.6 → 0.2.8
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/CHANGELOG.md +170 -103
- package/README.md +27 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +6 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate-rules.d.ts +6 -0
- package/dist/commands/validate-rules.d.ts.map +1 -0
- package/dist/commands/validate-rules.js +33 -0
- package/dist/commands/validate-rules.js.map +1 -0
- package/dist/configurators/cursor2plus-local.d.ts.map +1 -1
- package/dist/configurators/cursor2plus-local.js +2 -1
- package/dist/configurators/cursor2plus-local.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +8 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.2.2.json +9 -0
- package/dist/migrations/manifests/0.2.4.json +9 -0
- package/dist/migrations/manifests/0.2.5.json +9 -0
- package/dist/migrations/manifests/0.2.6.json +9 -0
- package/dist/migrations/manifests/0.2.7.json +9 -0
- package/dist/migrations/manifests/0.2.8.json +9 -0
- package/dist/templates/common/bundled-skills/trellis-cursor2plus-setup/SKILL.md +2 -2
- package/dist/templates/common/commands/finish-work.md +12 -0
- package/dist/templates/common/skills/brainstorm.md +164 -163
- package/dist/templates/common/skills/check.md +4 -0
- package/dist/templates/cursor/agents/trellis-check.md +32 -0
- package/dist/templates/cursor/agents/trellis-implement.md +11 -0
- package/dist/templates/cursor/agents/trellis-research.md +29 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.js +26 -0
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
- package/dist/templates/cursor/rules/trellis-subagent-dispatch.mdc +34 -0
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +1 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +43 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +102 -101
- package/dist/templates/trellis/config/execution-strategy-rules.json +31 -0
- package/dist/templates/trellis/index.d.ts +2 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +3 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/local/README.md +61 -37
- package/dist/templates/trellis/local/index.d.ts +1 -0
- package/dist/templates/trellis/local/index.d.ts.map +1 -1
- package/dist/templates/trellis/local/index.js +1 -0
- package/dist/templates/trellis/local/index.js.map +1 -1
- package/dist/templates/trellis/local/patch_wpelc8.py +162 -38
- package/dist/templates/trellis/local/smoke.py +94 -0
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
- package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +16 -2
- package/dist/templates/trellis/scripts/common/execution_strategy.py +268 -0
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +26 -7
- package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +18 -4
- package/dist/templates/trellis/scripts/common/task_store.py +1594 -1565
- package/dist/templates/trellis/scripts/task.py +918 -877
- package/dist/templates/trellis/workflow.md +805 -800
- package/dist/utils/codebase-retrieval-router.js +2 -2
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/cursor-retrieval-env.d.ts +2 -0
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
- package/dist/utils/cursor-retrieval-env.js +11 -2
- package/dist/utils/cursor-retrieval-env.js.map +1 -1
- package/dist/utils/mirror-check.d.ts +22 -0
- package/dist/utils/mirror-check.d.ts.map +1 -0
- package/dist/utils/mirror-check.js +90 -0
- package/dist/utils/mirror-check.js.map +1 -0
- package/dist/utils/normalize-text.d.ts +7 -0
- package/dist/utils/normalize-text.d.ts.map +1 -0
- package/dist/utils/normalize-text.js +16 -0
- package/dist/utils/normalize-text.js.map +1 -0
- package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +14 -7
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.d.ts +12 -3
- package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.js +37 -1
- package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
- package/dist/utils/retrieval-tool-classification.d.ts +2 -0
- package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
- package/dist/utils/retrieval-tool-classification.js +13 -0
- package/dist/utils/retrieval-tool-classification.js.map +1 -1
- package/dist/utils/semantic-plan-gate.d.ts.map +1 -1
- package/dist/utils/semantic-plan-gate.js +11 -4
- package/dist/utils/semantic-plan-gate.js.map +1 -1
- package/dist/utils/validate-rules.d.ts +21 -0
- package/dist/utils/validate-rules.d.ts.map +1 -0
- package/dist/utils/validate-rules.js +88 -0
- package/dist/utils/validate-rules.js.map +1 -0
- package/package.json +3 -2
|
@@ -1,59 +1,83 @@
|
|
|
1
|
-
# Cursor++
|
|
1
|
+
# Cursor++ Local Bundle
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
> **Native Cursor users:** safe to ignore or delete this directory unless using **Cursor++ BYOK**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Who needs this:** Cursor IDE with [Cursor++](https://ccursor.cometix.dev) (BYOK proxy). Method 2.5 patches the `WPeLc8` resolver so Trellis Task subagents get per-role BYOK slugs instead of inheriting the parent model.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Bundle files
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
| File | Purpose |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| `patch_wpelc8.py` | Operator CLI: dry-run map, compat check, apply (with `--approve`), revert |
|
|
12
|
+
| `smoke.py` | Health smoke: env, patch status, configured roles (no secret reads) |
|
|
13
|
+
| `trellis_task_models_config.py` | JSON5 + providers catalog resolver (used by patch) |
|
|
14
|
+
| `config.local.json.example` | Optional paths (`extensionJs`, `ccursorHome`, …) |
|
|
15
|
+
| `config.local.json` | Your local paths (gitignored; never commit secrets) |
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
User-wide model doc: `~/.ccursor/trellis-task-models.json5` (see `../trellis-task-models.json5.example`). Project override: `../subagent-models.json` (optional).
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|-----------------|--------|
|
|
15
|
-
| `trellis-research` | Trellis research Task subagent |
|
|
16
|
-
| `trellis-implement` | Trellis implement (+ Parent/Child default Task) |
|
|
17
|
-
| `trellis-check` | Trellis check Task subagent |
|
|
18
|
-
| `generalPurpose` / `shell` / `best-of-n-runner` | Cursor built-in Task types |
|
|
19
|
+
## Compatibility window
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
- **Verified:** Cursor **3.7.27** / Cursor++ **v0.0.11** (see `.trellis/spec/guides/cursor-subagent-policy.md`, 2026-06-18)
|
|
22
|
+
- **Re-verify** after any Cursor or Cursor++ upgrade: `python patch_wpelc8.py --check-compat`
|
|
23
|
+
- If `--check-compat` reports `fail` or `unknown`, treat the patch as stale until you re-apply or revert
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
python patch_wpelc8.py --explain # key meanings
|
|
24
|
-
python patch_wpelc8.py --list-models # current catalog from providers.json
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**Explore** is not in this file — use the Cursor++ panel.
|
|
25
|
+
## Method 2.5 workflow
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
1. **Dry-run map** — `python patch_wpelc8.py --print-map` (no file writes)
|
|
28
|
+
2. **Operator approval** — review map and compat output; decide to proceed
|
|
29
|
+
3. **Apply** — `python patch_wpelc8.py --apply --approve` (writes `extension.js`)
|
|
30
|
+
4. **Reload Cursor** — Developer: Reload Window (once per apply/revert)
|
|
31
|
+
5. **A ≠ B probe** — parent chat model must differ from a patched subagent map target; dispatch a `trellis-*` Task and confirm the subagent self-reports the **map target** slug/model, not the parent
|
|
32
|
+
6. **Revert if needed** — `python patch_wpelc8.py --revert` then Reload Window
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
Agent-led setup: run skill **`trellis-cursor2plus-setup`** in Cursor Agent (lists models, writes JSON5, runs patch steps).
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
2. `python patch_wpelc8.py` → Reload Window only if you had not patched yet or reverted
|
|
36
|
+
## Revert recipe
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
```bash
|
|
39
|
+
cd .trellis/local/cursor2plus
|
|
40
|
+
python patch_wpelc8.py --revert
|
|
41
|
+
```
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
Then **Developer: Reload Window**. Subagents return to **inherit parent** BYOK model. Re-apply only after `--check-compat` is `ok` and you have explicit `--approve`.
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|------|--------|
|
|
42
|
-
| `~/.ccursor/trellis-task-models.json5` (or `.json`) | User — all repos |
|
|
43
|
-
| `{repo}/.trellis/local/subagent-models.json` | Project overrides (optional) |
|
|
45
|
+
## A ≠ B verification recipe
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
Goal: prove the patch routes **subagent** model independently of the **parent** session model.
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
1. Set parent chat to model **A** (any BYOK model in Cursor++).
|
|
50
|
+
2. Ensure `trellis-task-models.json5` maps e.g. `trellis-implement` to model **B** (`primary`/`fallback` resolving to a different slug than A).
|
|
51
|
+
3. `python patch_wpelc8.py --print-map` — note the slug for `trellis-implement` (= map target **B**).
|
|
52
|
+
4. `python patch_wpelc8.py --apply --approve` → Reload Window.
|
|
53
|
+
5. Dispatch a `trellis-implement` Task from a parent on model **A**.
|
|
54
|
+
6. **Pass:** subagent self-report / trace shows **B** (map target), not **A**.
|
|
55
|
+
7. **Fail:** subagent shows **A** → patch stale or not applied; run `--check-compat`, re-patch, or `--revert`.
|
|
48
56
|
|
|
49
|
-
##
|
|
57
|
+
## CLI reference
|
|
50
58
|
|
|
51
59
|
```bash
|
|
52
|
-
|
|
53
|
-
python patch_wpelc8.py --
|
|
54
|
-
python patch_wpelc8.py
|
|
60
|
+
python patch_wpelc8.py --print-map # dry-run slug map
|
|
61
|
+
python patch_wpelc8.py --check-compat # WPeLc8 locatability (no writes)
|
|
62
|
+
python patch_wpelc8.py --apply --approve # write patch (requires --approve)
|
|
63
|
+
python patch_wpelc8.py --apply --dry-run # validate without write
|
|
64
|
+
python patch_wpelc8.py --revert # remove patch
|
|
65
|
+
python patch_wpelc8.py --explain # JSON5 key meanings
|
|
66
|
+
python patch_wpelc8.py --list-models # catalog from providers.json
|
|
67
|
+
python smoke.py # env + patch_status smoke
|
|
55
68
|
```
|
|
56
69
|
|
|
57
|
-
|
|
70
|
+
`--apply` without `--approve` **refuses** and exits non-zero.
|
|
71
|
+
|
|
72
|
+
## What to put in `trellis-task-models.json5`
|
|
73
|
+
|
|
74
|
+
| JSON key (under `models`) | Meaning |
|
|
75
|
+
|---------------------------|---------|
|
|
76
|
+
| `trellis-research` | Trellis research Task subagent |
|
|
77
|
+
| `trellis-implement` | Trellis implement (+ Parent/Child default Task) |
|
|
78
|
+
| `trellis-check` | Trellis check Task subagent |
|
|
79
|
+
| `generalPurpose` / `shell` / `best-of-n-runner` | Cursor built-in Task types |
|
|
80
|
+
|
|
81
|
+
Each role: `{ "primary": "<name>", "fallback": "<name>" }`. Names: **apiModel** or **displayName** from Cursor++ (`--list-models`). **Explore** is not in this file — use the Cursor++ panel.
|
|
58
82
|
|
|
59
|
-
Policy: `.trellis/spec/guides/cursor-subagent-policy.md` (Method 2.5 / 2.6).
|
|
83
|
+
Policy: `.trellis/spec/guides/cursor-subagent-policy.md` (Method 2.5 / 2.6).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const cursor2plusPatchScript: string;
|
|
2
2
|
export declare const cursor2plusReadme: string;
|
|
3
|
+
export declare const cursor2plusSmokeScript: string;
|
|
3
4
|
export declare const cursor2plusConfigExample: string;
|
|
4
5
|
export declare const subagentModelsExample: string;
|
|
5
6
|
export declare const trellisTaskModelsJson5Example: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAyB,CAAC;AACxD,eAAO,MAAM,wBAAwB,QAAyC,CAAC;AAC/E,eAAO,MAAM,qBAAqB,QAA4C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,QAEzC,CAAC;AACF,eAAO,MAAM,yBAAyB,QAA6C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAyB,CAAC;AACxD,eAAO,MAAM,sBAAsB,QAAwB,CAAC;AAC5D,eAAO,MAAM,wBAAwB,QAAyC,CAAC;AAC/E,eAAO,MAAM,qBAAqB,QAA4C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,QAEzC,CAAC;AACF,eAAO,MAAM,yBAAyB,QAA6C,CAAC"}
|
|
@@ -7,6 +7,7 @@ function readLocal(name) {
|
|
|
7
7
|
}
|
|
8
8
|
export const cursor2plusPatchScript = readLocal("patch_wpelc8.py");
|
|
9
9
|
export const cursor2plusReadme = readLocal("README.md");
|
|
10
|
+
export const cursor2plusSmokeScript = readLocal("smoke.py");
|
|
10
11
|
export const cursor2plusConfigExample = readLocal("config.local.json.example");
|
|
11
12
|
export const subagentModelsExample = readLocal("subagent-models.json.example");
|
|
12
13
|
export const trellisTaskModelsJson5Example = readLocal("trellis-task-models.json5.example");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAC,2BAA2B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC,8BAA8B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,SAAS,CACpD,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,SAAS,CAAC,+BAA+B,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAC5D,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAC,2BAA2B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC,8BAA8B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,SAAS,CACpD,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,SAAS,CAAC,+BAA+B,CAAC,CAAC"}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
141
|
+
if sys.platform == "darwin":
|
|
142
|
+
return [
|
|
129
143
|
Path("/Applications/Cursor.app/Contents/Resources/app/extensions/cursor2plus/dist/extension.js"),
|
|
130
144
|
]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
|
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
|
|
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":
|
|
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
|
}
|