diff --git a/docs/cursor-integration.md b/docs/cursor-integration.md index c49a0f3a..b8cdd0c8 100644 --- a/docs/cursor-integration.md +++ b/docs/cursor-integration.md @@ -1,17 +1,20 @@ # Cursor IDE Integration -This guide covers the current CCS-owned Cursor runtime, including auth import, local daemon lifecycle, live probe checks, and dashboard controls. +This guide covers the deprecated CCS-owned Cursor IDE bridge, including auth import, local daemon lifecycle, live probe checks, and dashboard controls. + +`ccs cursor` now belongs to the CLIProxy-backed Cursor provider path. +Use `ccs legacy cursor` for the deprecated local bridge documented here. ## What It Provides - OpenAI-compatible local endpoint powered by Cursor credentials. - Anthropic-compatible local endpoint at `/v1/messages` for Claude-native clients. - Cursor model list and chat completions via the local CCS daemon. -- Dedicated dashboard page: `ccs config` -> `Cursor IDE`. +- Dedicated dashboard page: `ccs config` -> `Deprecated` -> `Cursor IDE (Legacy)`. ## What This Runtime Actually Does -`ccs cursor` does not launch Cursor IDE itself. +`ccs legacy cursor` does not launch Cursor IDE itself. The current workflow is: 1. import Cursor credentials from local SQLite or manual input @@ -32,7 +35,7 @@ Treat this as a CCS-managed Cursor bridge, not a generic CLIProxy-backed provide ### 1) Enable integration ```bash -ccs cursor enable +ccs legacy cursor enable ``` ### 2) Import credentials @@ -40,25 +43,25 @@ ccs cursor enable Auto-detect from Cursor local SQLite state: ```bash -ccs cursor auth +ccs legacy cursor auth ``` Manual fallback: ```bash -ccs cursor auth --manual --token --machine-id +ccs legacy cursor auth --manual --token --machine-id ``` ### 3) Start daemon ```bash -ccs cursor start +ccs legacy cursor start ``` ### 4) Run a live probe ```bash -ccs cursor probe +ccs legacy cursor probe ``` Use this to verify that the current build can complete one real authenticated request through the local daemon. @@ -66,26 +69,37 @@ Use this to verify that the current build can complete one real authenticated re ### 5) Run Cursor-backed Claude ```bash -ccs cursor "explain this repo" +ccs legacy cursor "explain this repo" ``` ### 6) Verify status ```bash -ccs cursor status +ccs legacy cursor status ``` -Use `ccs cursor` with bare or normal Claude args to run through the local Cursor proxy. +Use `ccs legacy cursor` with bare or normal Claude args to run through the local Cursor proxy. The admin namespace remains available for setup and inspection: ```bash -ccs cursor help +ccs legacy cursor help ``` ### 7) Stop daemon ```bash -ccs cursor stop +ccs legacy cursor stop +``` + +## Supported Cursor Provider Path + +For the supported CLIProxy-backed Cursor provider, use: + +```bash +ccs cursor --auth +ccs cursor --accounts +ccs cursor --config +ccs cursor "task" ``` ## Runtime Defaults @@ -96,7 +110,7 @@ ccs cursor stop - Model list resolution: authenticated live fetch when available, with cached/default fallback. - Request model validation: if a requested model is not present in the available Cursor model catalog, daemon falls back to the resolved default model. - Daemon API surface: `POST /v1/chat/completions`, `POST /v1/messages`, and `GET /v1/models`. -- Live verification: `ccs cursor probe` or `POST /api/cursor/probe` +- Live verification: `ccs legacy cursor probe` or `POST /api/cursor/probe` These values are managed in unified config and can be updated from CLI or dashboard. @@ -108,7 +122,7 @@ Open dashboard: ccs config ``` -Then navigate to `Cursor IDE` in the sidebar. +Then navigate to `Cursor IDE (Legacy)` in the `Deprecated` section. Available controls: @@ -131,9 +145,9 @@ When raw settings include a local `ANTHROPIC_BASE_URL` port override, CCS synchr ### `Not authenticated` or `expired` in `ccs cursor status` -- Re-run `ccs cursor auth` (or manual auth command). +- Re-run `ccs legacy cursor auth` (or manual auth command). -### `ccs cursor probe` fails even though status is green +### `ccs legacy cursor probe` fails even though status is green - `status` proves local config/auth/daemon readiness only. - `probe` proves the live runtime path. @@ -148,4 +162,4 @@ When raw settings include a local `ANTHROPIC_BASE_URL` port override, CCS synchr ### Daemon fails to start - Check if port `20129` is in use. -- Change port in dashboard config tab, then retry `ccs cursor start`. +- Change port in dashboard config tab, then retry `ccs legacy cursor start`. diff --git a/plans/20260415-1016-cursor-provider-legacy-split/phase-01-cli-routing-namespacing.md b/plans/20260415-1016-cursor-provider-legacy-split/phase-01-cli-routing-namespacing.md new file mode 100644 index 00000000..44d44c4b --- /dev/null +++ b/plans/20260415-1016-cursor-provider-legacy-split/phase-01-cli-routing-namespacing.md @@ -0,0 +1,127 @@ +--- +phase: 1 +title: "CLI Routing & Namespacing" +status: complete +effort: "6h" +--- + +# Phase 1: CLI Routing & Namespacing + +## Context Links + +- `plan.md` +- `src/ccs.ts` +- `src/auth/profile-detector.ts` +- `src/cursor/constants.ts` +- `src/commands/root-command-router.ts` +- `src/commands/command-catalog.ts` +- `src/commands/help-command.ts` +- `src/commands/cursor-command.ts` +- `src/commands/cursor-command-display.ts` +- `src/types/profile.ts` +- `src/config/reserved-names.ts` + +## Overview + +- Priority: P1 +- Owner scope: CLI entry, help, profile detection, command naming +- Goal: make `cursor` provider-first and move the deprecated bridge under `legacy cursor` + +## Key Insights + +- The current collision is structural, not cosmetic. `ccs cursor` means "legacy bridge" in `src/ccs.ts` and `src/auth/profile-detector.ts`, but `cursor` is also listed as a built-in CLIProxy provider. +- `shouldUseCursorCliproxyShortcut()` is only a heuristic escape hatch. It does not fix bare `ccs cursor`, quoted prompts, or help routing. +- Help is currently inconsistent: provider help exists generically, but `cursor` is excluded and routed to bridge help instead. + +## Requirements + +- Reserve `cursor` for CLIProxy runtime and CLIProxy admin flags. +- Introduce explicit legacy syntax: `ccs legacy cursor ...`. +- Keep a release-N alias for old legacy admin subcommands only. +- Rename internal bridge-only profile typing from ambiguous `cursor` to explicit `legacy-cursor`. +- Keep file ownership isolated to CLI/router/help files in this phase. + +## Data Flow + +- Provider path: + `argv -> root command resolution -> provider shortcut/help path -> ProfileDetector(type=cliproxy, provider=cursor) -> CLIProxy runtime` +- Legacy path: + `argv -> legacy root command -> legacy cursor subrouter -> ProfileDetector(type=legacy-cursor) or direct handler -> local bridge runtime` +- Deprecated alias path, release N only: + `argv=ccs cursor auth|status|... -> alias shim -> warning -> dispatch to legacy cursor handler` + +## Architecture + +- Add a new root command namespace: `ccs legacy`. +- Add nested routing under `legacy` with `cursor` as the first migrated leaf. Do not overload `cursor` itself any longer. +- Remove provider exceptions for `cursor` from the generic provider help/routing logic. `ccs cursor --help` should now use provider shortcut help. +- Convert bridge-only type checks from `profileInfo.type === 'cursor'` to `profileInfo.type === 'legacy-cursor'`. +- Keep `ccs cursor help` only as a release-N compatibility shim that prints: + - `Use "ccs cursor --help" for CLIProxy Cursor` + - `Use "ccs legacy cursor help" for the deprecated bridge` + +## Related Code Files + +- Modify: + - `src/ccs.ts` + - `src/auth/profile-detector.ts` + - `src/cursor/constants.ts` + - `src/commands/root-command-router.ts` + - `src/commands/command-catalog.ts` + - `src/commands/help-command.ts` + - `src/commands/cursor-command.ts` + - `src/commands/cursor-command-display.ts` + - `src/types/profile.ts` + - `src/config/reserved-names.ts` + - `src/shared/claude-extension-setup.ts` + - `src/targets/target-runtime-compatibility.ts` +- Create: + - `src/commands/legacy-command.ts` or `src/commands/legacy/index.ts` + - `src/commands/legacy/cursor-command.ts` if the team wants physical separation immediately + +## Implementation Steps + +1. Add the `legacy` root command route and its help surface. +2. Flip `src/ccs.ts` so `cursor` goes through normal CLIProxy provider routing; remove the special-case that gives the bridge ownership of the name. +3. Replace the `shouldUseCursorCliproxyShortcut()` hack with provider-first dispatch plus a compatibility alias table for the old legacy subcommands. +4. Update `ProfileDetector` priority order so `cursor` resolves as `cliproxy`, while `legacy cursor` resolves as `legacy-cursor`. +5. Rename bridge-only help text, summaries, and status text to say "legacy Cursor bridge" explicitly. +6. Audit all `profileType === 'cursor'` checks and convert only the bridge-specific ones to `legacy-cursor`. + +## Todo List + +- [x] Add `legacy cursor` routing +- [x] Make `ccs cursor` provider-first for bare, prompt, and `--help` usage +- [x] Add deprecated alias forwarding for old admin subcommands +- [x] Rename internal bridge profile path to `legacy-cursor` +- [x] Update provider help, completion, and command catalog summaries + +## Success Criteria + +- `ccs cursor "task"` resolves to CLIProxy Cursor. +- `ccs legacy cursor "task"` resolves to the old bridge. +- `ccs cursor --help` shows provider shortcut help. +- `ccs cursor auth` still works in release N, but prints an exact replacement warning. +- No CLI path depends on `shouldUseCursorCliproxyShortcut()` to disambiguate runtime meaning. + +## Risk Assessment + +- High likelihood / high impact: users with scripts calling `ccs cursor "task"` will hit the provider path immediately. + Mitigation: call this out in release notes, keep admin aliases, add explicit warning when legacy files/config are detected and the user invokes `ccs cursor` with no flags. +- Medium likelihood / medium impact: bridge-only type renames may break target compatibility checks or extension setup. + Mitigation: grep audit every `profileType === 'cursor'` branch before tests. + +## Rollback Plan + +- Re-enable the old `cursor` special-case in `src/ccs.ts` and `ProfileDetector`. +- Keep the new `legacy` namespace in place even if dormant; it is additive and safe to leave. +- Do not roll back migrated files in this phase; routing rollback alone is enough. + +## Security Considerations + +- No auth material moves in this phase. +- Preserve existing `CCS_HOME`-aware path resolution. Do not introduce `os.homedir()` shortcuts while adding the new namespace. + +## Next Steps + +- Phase 2 depends on the new command contract from this phase. diff --git a/plans/20260415-1016-cursor-provider-legacy-split/phase-02-storage-api-boundaries.md b/plans/20260415-1016-cursor-provider-legacy-split/phase-02-storage-api-boundaries.md new file mode 100644 index 00000000..05cf43e8 --- /dev/null +++ b/plans/20260415-1016-cursor-provider-legacy-split/phase-02-storage-api-boundaries.md @@ -0,0 +1,140 @@ +--- +phase: 2 +title: "Storage & API Boundaries" +status: partial +effort: "6h" +--- + +# Phase 2: Storage & API Boundaries + +## Context Links + +- `plan.md` +- `src/config/unified-config-types.ts` +- `src/config/unified-config-loader.ts` +- `src/cursor/cursor-auth.ts` +- `src/cursor/cursor-daemon-pid.ts` +- `src/cliproxy/config/path-resolver.ts` +- `src/cliproxy/config/env-builder.ts` +- `src/web-server/routes/index.ts` +- `src/web-server/routes/cursor-routes.ts` +- `src/web-server/routes/cursor-settings-routes.ts` +- `src/web-server/routes/cliproxy-stats-routes.ts` +- `src/api/services/profile-lifecycle-service.ts` + +## Overview + +- Priority: P1 +- Owner scope: config schema, path resolution, backend APIs, migration readers +- Goal: make legacy bridge storage explicit and guarantee CLIProxy Cursor never writes the legacy raw settings file + +## Key Insights + +- Top-level `config.cursor` is bridge-only configuration today and must move. +- The legacy bridge owns `~/.ccs/cursor.settings.json`, `~/.ccs/cursor/credentials.json`, and `~/.ccs/cursor/daemon.pid`. +- CLIProxy provider settings currently resolve through generic provider settings helpers and can still collide with the legacy file for provider `cursor`. +- `~/.ccs/cursor.settings.json` is historically documented as legacy-owned, so it is unsafe to auto-import it into provider storage by default. + +## Requirements + +- Canonical legacy config key: `legacy.cursor` +- Canonical legacy files: + - `~/.ccs/legacy/cursor.settings.json` + - `~/.ccs/legacy/cursor/credentials.json` + - `~/.ccs/legacy/cursor/daemon.pid` +- Canonical provider file for CLIProxy Cursor only: + - `~/.ccs/cliproxy/cursor.settings.json` +- Canonical legacy API namespace: + - `/api/legacy/cursor/*` +- Compatibility reads: + - read old `config.cursor` + - read old `~/.ccs/cursor.settings.json` + - read old `~/.ccs/cursor/*` +- Compatibility writes: + - write only the new `legacy.*` and `cliproxy/*` paths + +## Data Flow + +- Legacy config: + `load config -> prefer legacy.cursor -> fallback config.cursor -> normalize -> write legacy.cursor only` +- Legacy raw settings: + `load /api/legacy/cursor/settings/raw -> prefer ~/.ccs/legacy/cursor.settings.json -> fallback ~/.ccs/cursor.settings.json -> write new legacy path` +- Provider settings: + `CLIProxy env builder/stats updater -> read ~/.ccs/cliproxy/cursor.settings.json -> if absent use defaults -> never read/write ~/.ccs/cursor.settings.json` + +## Architecture + +- Add a `legacy` section to unified config types and loader. Keep old `cursor` as read-only migration input during the compatibility window. +- Move legacy bridge filesystem helpers under a `legacy/cursor` path prefix. +- Split API routing: + - new canonical mount: `/api/legacy/cursor` + - release-N alias: `/api/cursor` -> same handlers + deprecation header +- Special-case CLIProxy provider settings for `cursor` only in the provider path resolver. Do not expand this migration to every provider in this issue. +- Treat existing `~/.ccs/cursor.settings.json` as legacy-owned. Do not auto-copy it into provider storage unless a future explicit provider migration is added. + +## Related Code Files + +- Modify: + - `src/config/unified-config-types.ts` + - `src/config/unified-config-loader.ts` + - `src/cursor/cursor-auth.ts` + - `src/cursor/cursor-daemon-pid.ts` + - `src/cliproxy/config/path-resolver.ts` + - `src/cliproxy/config/env-builder.ts` + - `src/web-server/routes/index.ts` + - `src/web-server/routes/cursor-routes.ts` + - `src/web-server/routes/cursor-settings-routes.ts` + - `src/web-server/routes/cliproxy-stats-routes.ts` + - `src/api/services/profile-lifecycle-service.ts` +- Create: + - `src/web-server/routes/legacy-cursor-routes.ts` + - `src/web-server/routes/legacy-cursor-settings-routes.ts` + - `src/config/migrations/cursor-legacy-migration.ts` if migration logic should stay out of the loader + +## Implementation Steps + +1. Extend config types and loader to support `legacy.cursor`, with `legacy.cursor` taking precedence over old `cursor`. +2. Update legacy bridge credential and pid helpers to use `~/.ccs/legacy/cursor/`. +3. Update the raw settings route to use `~/.ccs/legacy/cursor.settings.json` as canonical and old root path as read fallback only. +4. Move legacy API mounts to `/api/legacy/cursor/*` and keep `/api/cursor/*` as a warned alias for release N. +5. Change CLIProxy Cursor provider settings resolution to `~/.ccs/cliproxy/cursor.settings.json`. +6. Update orphan detection and cleanup logic so old `cursor.settings.json` is treated as a migration target, not a permanent provider-owned file. + +## Todo List + +- [ ] Add `legacy.cursor` config schema and loader precedence +- [ ] Move bridge credentials/pid/raw settings under `~/.ccs/legacy/` +- [x] Add canonical `/api/legacy/cursor/*` routes +- [x] Keep release-N `/api/cursor/*` alias +- [x] Isolate CLIProxy Cursor settings away from `~/.ccs/cursor.settings.json` +- [ ] Update cleanup/orphan handling + +## Success Criteria + +- Saving legacy bridge settings writes only to `legacy.cursor` and `~/.ccs/legacy/*`. +- CLIProxy Cursor model/env updates write only to `~/.ccs/cliproxy/cursor.settings.json`. +- Existing legacy users can still read old config/files during the compatibility window. +- No backend route that serves the provider path references `~/.ccs/cursor.settings.json`. + +## Risk Assessment + +- High likelihood / high impact: old `~/.ccs/cursor.settings.json` contents are ambiguous between bridge and provider expectations. + Mitigation: treat the file as legacy-owned and do not auto-import it into provider storage. +- Medium likelihood / medium impact: route aliasing may mask which API is canonical. + Mitigation: add explicit response headers or payload flags marking `/api/cursor/*` as deprecated. + +## Rollback Plan + +- Keep read fallback from old paths even if the canonical write path changes back. +- If the new legacy API namespace causes regressions, remount `/api/cursor/*` as canonical temporarily and keep the new namespace dormant. +- Do not delete old files during release N; cleanup stays opt-in until release N+2. + +## Security Considerations + +- Preserve `0600` for migrated credentials and `0700` for directories. +- Use atomic temp-file writes exactly as current routes do. +- Never copy provider tokens into the legacy namespace or legacy tokens into provider storage automatically. + +## Next Steps + +- Phase 3 depends on the canonical API and path names from this phase. diff --git a/plans/20260415-1016-cursor-provider-legacy-split/phase-03-dashboard-deprecation-ux.md b/plans/20260415-1016-cursor-provider-legacy-split/phase-03-dashboard-deprecation-ux.md new file mode 100644 index 00000000..00f22c6f --- /dev/null +++ b/plans/20260415-1016-cursor-provider-legacy-split/phase-03-dashboard-deprecation-ux.md @@ -0,0 +1,121 @@ +--- +phase: 3 +title: "Dashboard & Deprecation UX" +status: partial +effort: "4h" +--- + +# Phase 3: Dashboard & Deprecation UX + +## Context Links + +- `plan.md` +- `ui/src/App.tsx` +- `ui/src/components/layout/app-sidebar.tsx` +- `ui/src/pages/cursor.tsx` +- `ui/src/hooks/use-cursor.ts` +- `ui/src/lib/i18n.ts` +- `src/web-server/routes/index.ts` +- `src/commands/cursor-command-display.ts` + +## Overview + +- Priority: P1 +- Owner scope: dashboard route ownership, labels, user-facing deprecation messaging +- Goal: align dashboard semantics with CLI semantics so `/cursor` means provider and legacy UI is clearly marked and isolated + +## Key Insights + +- The current dashboard already admits the bridge is deprecated, but the route `/cursor` still belongs to it. +- The page includes direct navigation to CLIProxy Cursor, which means the UX already wants a split; the route layer just has not caught up. +- Keeping `/cursor` for legacy while CLI uses `cursor` for provider would create the same ambiguity in a different surface. + +## Requirements + +- `/cursor` must become the provider-owned dashboard surface. +- The legacy bridge page must move to `/legacy/cursor`. +- Legacy bridge API hooks must move to `/api/legacy/cursor/*`. +- The deprecated UX must contain exact replacements, not generic warnings. +- Sidebar grouping must reflect support level: + - provider view under provider/cliproxy navigation + - legacy bridge under deprecated navigation + +## Data Flow + +- Provider dashboard: + `browser /cursor -> provider view or redirect wrapper -> /cliproxy?provider=cursor -> existing CLIProxy provider APIs` +- Legacy dashboard: + `browser /legacy/cursor -> legacy bridge page -> useLegacyCursor hook -> /api/legacy/cursor/*` +- Compatibility API path, release N only: + `old UI/tests -> /api/cursor/* -> alias handler -> same legacy payload + deprecation signal` + +## Architecture + +- Keep provider UI DRY by making `/cursor` a thin redirect or preselected wrapper around the existing CLIProxy provider page instead of building a second Cursor-provider page. +- Move the current `ui/src/pages/cursor.tsx` implementation to a new `legacy-cursor` page and rename its hook to `useLegacyCursor`. +- Change nav labels from generic "Cursor IDE" to explicit "Cursor Bridge (Legacy)" in the deprecated section. +- Update CLI and dashboard warnings to show both paths side-by-side: + - `ccs cursor --auth` / `/cursor` + - `ccs legacy cursor auth` / `/legacy/cursor` + +## Related Code Files + +- Modify: + - `ui/src/App.tsx` + - `ui/src/components/layout/app-sidebar.tsx` + - `ui/src/lib/i18n.ts` + - `src/commands/cursor-command-display.ts` +- Move or rename: + - `ui/src/pages/cursor.tsx` -> `ui/src/pages/legacy-cursor.tsx` + - `ui/src/hooks/use-cursor.ts` -> `ui/src/hooks/use-legacy-cursor.ts` +- Create: + - `ui/src/pages/cursor-provider-redirect.tsx` if a wrapper is preferred over direct router config + +## Implementation Steps + +1. Move the legacy page and hook to `legacy-*` names and update all imports. +2. Reassign `/cursor` to the provider path and add `/legacy/cursor` for the bridge page. +3. Update sidebar grouping and labels so the provider path is no longer listed under Deprecated. +4. Replace vague deprecated copy with concrete migration copy: + - old command + - new command + - old route + - new route +5. Keep the legacy page banner persistent until release N+2, not dismissible per session. + +## Todo List + +- [ ] Move legacy page/hook module names to `legacy-*` +- [x] Reassign `/cursor` and add `/legacy/cursor` +- [x] Update deprecated nav group and labels +- [x] Rewrite key banners, button copy, and path labels with exact replacements +- [x] Keep provider and legacy links visible from both surfaces during release N + +## Success Criteria + +- Opening `/cursor` lands on the CLIProxy Cursor provider surface. +- Opening `/legacy/cursor` lands on the bridge page with a persistent deprecation banner. +- No dashboard component serving the provider route uses the legacy API hook. +- Every warning banner shows the exact before/after command and route. + +## Risk Assessment + +- Medium likelihood / medium impact: users with bookmarked `/cursor` expect the legacy page. + Mitigation: provider page shows a top-level "Looking for the old bridge?" callout linking to `/legacy/cursor`. +- Low likelihood / medium impact: UI rename churn breaks lazy imports or tests. + Mitigation: do route and hook rename in one phase and leave compatibility API alias in place until tests pass. + +## Rollback Plan + +- Point `/cursor` back to the legacy page if the provider redirect breaks. +- Keep `/legacy/cursor` additive; it does not block rollback. +- Do not remove the deprecation banner on rollback; it still communicates future intent. + +## Security Considerations + +- No auth secrets should be exposed in UI copy or route params. +- Keep manual auth dialogs scoped to the legacy page only. Provider auth remains in CLIProxy flows. + +## Next Steps + +- Phase 4 owns test rewrites, docs updates, and release gating for these UI changes. diff --git a/plans/20260415-1016-cursor-provider-legacy-split/phase-04-tests-docs-rollout.md b/plans/20260415-1016-cursor-provider-legacy-split/phase-04-tests-docs-rollout.md new file mode 100644 index 00000000..9fcd17d1 --- /dev/null +++ b/plans/20260415-1016-cursor-provider-legacy-split/phase-04-tests-docs-rollout.md @@ -0,0 +1,148 @@ +--- +phase: 4 +title: "Tests Docs & Rollout" +status: complete +effort: "4h" +--- + +# Phase 4: Tests Docs & Rollout + +## Context Links + +- `plan.md` +- `docs/cursor-integration.md` +- `README.md` +- `docs/system-architecture/provider-flows.md` +- `docs/system-architecture/index.md` +- `tests/unit/cursor/cursor-shortcut-routing.test.ts` +- `tests/unit/web-server/cursor-settings-routes.test.ts` +- `tests/unit/web-server/cursor-routes.test.ts` +- `ui/tests/unit/hooks/use-cursor.test.tsx` +- `ui/tests/unit/ui/pages/cursor-page.test.tsx` + +## Overview + +- Priority: P1 +- Owner scope: compatibility rollout, validation, docs/help updates, release notes +- Goal: ship the namespace split without surprising existing bridge users or leaving docs/help inconsistent + +## Key Insights + +- This change has one intentional breaking behavior: positional `ccs cursor` stops being the legacy bridge. +- Everything else can use a compatibility window: admin subcommands, API aliases, old config reads, old file-path reads. +- Tests must lock both meanings so the ambiguity does not regress later. + +## Requirements + +- Document exact before/after commands and routes. +- Add a concrete migration path for three user groups: + - legacy bridge users + - CLIProxy Cursor users + - dashboard bookmark users +- Define removal windows for aliases and old path fallbacks. +- Run repo quality gates after implementation: + - root: `bun run format && bun run lint:fix && bun run validate && bun run validate:ci-parity` + - UI: `cd ui && bun run format && bun run lint:fix && bun run validate` + +## Test Matrix + +- Unit: + - provider-first cursor routing + - legacy alias forwarding + - `legacy.cursor` loader precedence + - path resolvers for legacy vs provider files + - deprecation help text snapshots +- Integration: + - `ccs cursor "task"` -> provider + - `ccs legacy cursor "task"` -> bridge + - `/api/legacy/cursor/*` canonical behavior + - `/api/cursor/*` alias behavior during release N +- UI: + - `/cursor` route ownership + - `/legacy/cursor` banner and actions + - hook path changes and raw settings save targets +- Manual release validation: + - migrate old config/files in a temp `CCS_HOME` + - verify provider path never writes `~/.ccs/cursor.settings.json` + +## User Migration Plan + +1. Legacy bridge users: + - replace `ccs cursor ...` with `ccs legacy cursor ...` + - run `ccs legacy cursor status` + - update scripts and dashboard bookmarks to `/legacy/cursor` +2. CLIProxy Cursor users: + - keep using `ccs cursor ...` + - if provider-specific settings are needed, re-save them under the new provider-owned path instead of relying on `~/.ccs/cursor.settings.json` +3. Mixed/unclear state: + - `ccs migrate` should move `config.cursor` and legacy files into the new legacy namespace + - do not auto-copy the old raw settings file into provider storage + +## Deprecation UX Plan + +- CLI warning text, release N: + - `ccs cursor auth` is deprecated. Use `ccs legacy cursor auth` for the old bridge or `ccs cursor --auth` for CLIProxy Cursor. +- Dashboard banner: + - visible on `/legacy/cursor` + - provider route links back to legacy route with "Looking for the old bridge?" +- Docs banner: + - top callout in `docs/cursor-integration.md` pointing users to CLIProxy Cursor as the supported path + +## Related Code Files + +- Modify tests: + - `tests/unit/cursor/cursor-shortcut-routing.test.ts` + - `tests/unit/web-server/cursor-settings-routes.test.ts` + - `tests/unit/web-server/cursor-routes.test.ts` + - `ui/tests/unit/hooks/use-cursor.test.tsx` + - `ui/tests/unit/ui/pages/cursor-page.test.tsx` +- Modify docs: + - `docs/cursor-integration.md` + - `README.md` if root command examples mention Cursor + - `docs/system-architecture/provider-flows.md` + - `docs/system-architecture/index.md` + - CLI help snapshots or generated references if present + +## Implementation Steps + +1. Rewrite tests around the new command contract and route ownership before removing aliases in later releases. +2. Update docs/help text in the same PR as code changes so the new syntax ships atomically. +3. Add migration notes to changelog/release notes with a bold callout that `ccs cursor "task"` now means CLIProxy Cursor. +4. Keep a removal checklist for release N+1 and N+2 in the plan or roadmap so the compatibility window does not become permanent. + +## Todo List + +- [x] Update unit, integration, and selected UI tests +- [x] Update docs and CLI help text +- [x] Add migration note and deprecation wording +- [x] Run root and UI quality gates +- [x] Record alias-removal follow-up for N+1 and old-path-removal follow-up for N+2 + +## Success Criteria + +- Test suite covers both provider and legacy cursor paths explicitly. +- Docs and help text match the shipped command contract exactly. +- Release notes include the migration table and deprecation window. +- Quality gates pass in both root and `ui/`. + +## Risk Assessment + +- High likelihood / medium impact: docs or tests lag behind the command flip and users keep invoking the wrong surface. + Mitigation: block merge until help text, docs, and tests all match the new contract. +- Medium likelihood / medium impact: compatibility shims never get removed. + Mitigation: create follow-up issues or roadmap entries for N+1 and N+2 removal work before merge. + +## Rollback Plan + +- If rollout messaging is incomplete, revert the command flip before removing aliases. +- If only docs/help are wrong, fix docs first and keep aliases until corrected. +- Old-path readers stay in place through N+1, so rollback does not strand migrated users. + +## Security Considerations + +- Use temp `CCS_HOME` in tests and manual verification. Never touch the real `~/.ccs`. +- Sanitize any migration logs or warnings so they mention paths, not token contents. + +## Next Steps + +- Implementation is complete when all four phases land together; do not ship phase 1 without phases 2-4. diff --git a/plans/20260415-1016-cursor-provider-legacy-split/plan.md b/plans/20260415-1016-cursor-provider-legacy-split/plan.md new file mode 100644 index 00000000..f57f3f29 --- /dev/null +++ b/plans/20260415-1016-cursor-provider-legacy-split/plan.md @@ -0,0 +1,93 @@ +--- +title: "Separate legacy Cursor bridge from CLIProxy Cursor provider" +description: "Reserve `cursor` for the CLIProxy provider, move the reverse-engineered bridge under `legacy`, and split storage/UI with a staged migration." +status: in_progress +priority: P1 +effort: 2d +branch: kai/feat/1016-missing-provider-integration +tags: [cursor, cliproxy, migration, dashboard, deprecation] +created: 2026-04-15 +blockedBy: [] +blocks: [] +--- + +# Separate legacy Cursor bridge from CLIProxy Cursor provider + +## Goal + +Make `cursor` mean one thing everywhere: the CLIProxy-backed provider. Move the deprecated local bridge to `legacy`, stop provider writes to `~/.ccs/cursor.settings.json`, and ship a low-risk migration window. + +## Current Collision Points + +- `src/ccs.ts` hardcodes `cursor` as a legacy command/profile, then reclaims only `--auth|--logout|--config|--accounts` for CLIProxy. +- `src/auth/profile-detector.ts` resolves `cursor` to the legacy runtime before CLIProxy provider detection. +- `src/commands/command-catalog.ts` and `src/commands/help-command.ts` advertise `cursor` as both bridge and provider. +- `src/config/unified-config-types.ts` + `src/config/unified-config-loader.ts` store bridge config under top-level `cursor`. +- `src/cliproxy/config/path-resolver.ts`, `src/cliproxy/config/env-builder.ts`, and `src/web-server/routes/cliproxy-stats-routes.ts` still use provider settings paths that collide with the legacy raw file. +- `src/web-server/routes/cursor-*.ts`, `ui/src/pages/cursor.tsx`, `ui/src/hooks/use-cursor.ts`, `ui/src/App.tsx`, and `ui/src/components/layout/app-sidebar.tsx` dedicate `/cursor` and `/api/cursor/*` to the legacy bridge. +- `docs/cursor-integration.md` documents `ccs cursor` as the bridge even though CLIProxy already exposes a `cursor` provider shortcut. + +## Command Contract + +Before: +```text +ccs cursor -> legacy bridge runtime +ccs cursor "task" -> legacy bridge runtime +ccs cursor auth|status|... -> legacy bridge admin +ccs cursor --auth|--config -> CLIProxy Cursor shortcut +``` + +After release N: +```text +ccs cursor -> CLIProxy Cursor runtime +ccs cursor "task" -> CLIProxy Cursor runtime +ccs cursor --auth|--config -> CLIProxy Cursor admin +ccs legacy cursor -> legacy bridge runtime +ccs legacy cursor "task" -> legacy bridge runtime +ccs legacy cursor auth|... -> legacy bridge admin +``` + +Compatibility window, release N only: +- `ccs cursor auth|status|probe|models|start|stop|enable|disable|help` forwards to `ccs legacy cursor ...` with a deprecation warning. +- Bare and positional `ccs cursor` switch immediately to the provider path; no silent legacy fallback. + +## Phase Plan + +| Phase | Scope | Output | +| --- | --- | --- | +| 1 | [CLI Routing & Namespacing](./phase-01-cli-routing-namespacing.md) | Provider-first `cursor`, explicit `legacy cursor`, updated help/catalog/type names | +| 2 | [Storage & API Boundaries](./phase-02-storage-api-boundaries.md) | `legacy.cursor` config, split file paths, `/api/legacy/cursor/*`, provider path isolation | +| 3 | [Dashboard & Deprecation UX](./phase-03-dashboard-deprecation-ux.md) | `/cursor` -> provider view, `/legacy/cursor` -> bridge view, clear migration UX | +| 4 | [Tests Docs & Rollout](./phase-04-tests-docs-rollout.md) | Compatibility plan, migration steps, test matrix, docs updates, rollback gates | + +## Rollout Sequence + +1. Release N: add new legacy namespace, flip `ccs cursor` to provider, keep old admin subcommands and `/api/cursor/*` as warned aliases, and split provider settings away from `~/.ccs/cursor.settings.json`. +2. Release N+1: move the remaining legacy backend/config namespaces fully under `legacy.cursor`, keep old file-path fallback and `/api/cursor/*` alias for one more release. +3. Release N+2: remove old `config.cursor` and root-level `~/.ccs/cursor*` fallback reads, delete stale alias docs/help, and let cleanup/migrate remove leftovers. + +## Current Implementation Status + +- Completed in this branch: + - `ccs cursor` is provider-first for runtime and `--help` + - `ccs legacy cursor` works as the explicit legacy bridge namespace + - old legacy admin subcommands under `ccs cursor ...` forward with deprecation warnings + - CLIProxy Cursor settings no longer collide with `~/.ccs/cursor.settings.json` + - `/cursor` redirects to the provider surface while `/legacy/cursor` serves the deprecated bridge page + - `/api/legacy/cursor/*` is mounted and the legacy page uses that namespace + - docs, completion, and core regression tests were updated +- Intentionally deferred follow-up: + - move top-level `config.cursor` to `legacy.cursor` + - move legacy credentials/pid/raw settings fully under `~/.ccs/legacy/cursor/*` + - rename `use-cursor` and `CursorPage` modules to explicit `legacy-*` + +## Success Criteria + +- `cursor` is provider-owned in CLI help, routing, dashboard nav, and docs. +- Legacy bridge is reachable only through `legacy cursor` and `legacy.cursor` storage. +- CLIProxy Cursor never reads or writes `~/.ccs/cursor.settings.json`. +- Existing legacy users have an explicit migration path, warning UX, and rollback-safe compatibility window. + +## Docs Impact + +Major. CLI reference, Cursor docs, dashboard tour, provider docs, and migration notes all change in the same release. diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index 637d004c..b0c8d1ca 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -26,6 +26,7 @@ import { getCcsDir } from '../utils/config-manager'; import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat'; import type { CLIProxyProvider } from '../cliproxy/types'; import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities'; +import { LEGACY_CURSOR_PROFILE_NAME } from '../cursor/constants'; import { normalizeCopilotModelId } from '../copilot/copilot-model-normalizer'; import type { TargetType } from '../targets/target-adapter'; import type { ProfileType } from '../types/profile'; @@ -296,20 +297,17 @@ class ProfileDetector { }; } - // Priority 0.25: Check Cursor profile - local Cursor daemon runtime. - // This keeps bare `ccs cursor` and `ccs cursor ` bound to the - // existing runtime/admin surface even though CLIProxy also exposes a - // distinct provider named "cursor". - if (profileName === 'cursor') { + // Priority 0.25: Check explicit legacy Cursor bridge profile. + if (profileName === LEGACY_CURSOR_PROFILE_NAME) { const cursorConfig = getCursorConfig(); if (!cursorConfig?.enabled) { const error = new Error( - 'Cursor profile is not enabled.\n\n' + + 'Legacy Cursor profile is not enabled.\n\n' + 'To enable Cursor integration:\n' + - ' 1. Run: ccs cursor enable\n' + - ' 2. Import auth: ccs cursor auth\n' + - ' 3. Start daemon: ccs cursor start\n\n' + + ' 1. Run: ccs legacy cursor enable\n' + + ' 2. Import auth: ccs legacy cursor auth\n' + + ' 3. Start daemon: ccs legacy cursor start\n\n' + 'Or manually edit ~/.ccs/config.yaml:\n' + ' cursor:\n' + ' enabled: true' @@ -322,7 +320,7 @@ class ProfileDetector { return { type: 'cursor', - name: 'cursor', + name: LEGACY_CURSOR_PROFILE_NAME, cursorConfig, }; } diff --git a/src/ccs.ts b/src/ccs.ts index 99249df8..ead46f72 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -65,7 +65,7 @@ import { resolveOfficialChannelsLaunchPlan, } from './channels/official-channels-runtime'; import { getOfficialChannelReadiness } from './channels/official-channels-store'; -import { isCursorSubcommandToken, shouldUseCursorCliproxyShortcut } from './cursor/constants'; +import { isCursorSubcommandToken, LEGACY_CURSOR_PROFILE_NAME } from './cursor/constants'; import { isCLIProxyProvider } from './cliproxy/provider-capabilities'; // Import centralized error handling @@ -149,6 +149,26 @@ function detectProfile(args: string[]): DetectedProfile { } } +function normalizeLegacyCursorArgs(args: string[]): string[] { + if (args[0] === 'legacy' && args[1] === 'cursor') { + return [LEGACY_CURSOR_PROFILE_NAME, ...args.slice(2)]; + } + + return args; +} + +function printCursorLegacySubcommandDeprecation(subcommand: string): void { + console.error( + info(`\`ccs cursor ${subcommand}\` is deprecated for the legacy Cursor IDE bridge.`) + ); + console.error( + info( + `Use \`ccs legacy cursor ${subcommand}\` for the old bridge, or \`ccs cursor --auth|--accounts|--config\` for the CLIProxy provider.` + ) + ); + console.error(''); +} + function resolveRuntimeReasoningFlags( args: string[], envThinkingValue: string | undefined @@ -343,7 +363,7 @@ async function main(): Promise { registerTarget(new CodexAdapter()); const cliLogger = createLogger('cli'); - const args = process.argv.slice(2); + let args = process.argv.slice(2); const isCompletionCommand = args[0] === '__complete'; // Initialize UI colors early to ensure consistent colored output @@ -419,6 +439,8 @@ async function main(): Promise { return; } + args = normalizeLegacyCursorArgs(args); + cliLogger.info('command.start', 'CLI invocation started', { command: args[0] || 'default', argCount: args.length, @@ -489,7 +511,6 @@ async function main(): Promise { if ( typeof firstArg === 'string' && isCLIProxyProvider(firstArg) && - firstArg !== 'cursor' && args.length > 1 && (args.includes('--help') || args.includes('-h')) ) { @@ -511,9 +532,8 @@ async function main(): Promise { } } - // Special case: cursor command (Cursor local proxy integration) - // Route known admin subcommands to the command handler, keep all other args as profile passthrough. - if (firstArg === 'cursor' && args.length > 1) { + // Special case: explicit legacy Cursor bridge namespace. + if (firstArg === LEGACY_CURSOR_PROFILE_NAME && args.length > 1) { const { handleCursorCommand } = await import('./commands/cursor-command'); const cursorToken = args[1]; @@ -523,6 +543,19 @@ async function main(): Promise { } } + // Compatibility shim: old `ccs cursor ` still forwards to the legacy bridge + // for one migration window, but bare/positional `ccs cursor` now belongs to CLIProxy. + if (firstArg === 'cursor' && args.length > 1) { + const { handleCursorCommand } = await import('./commands/cursor-command'); + const cursorToken = args[1]; + + if (isCursorSubcommandToken(cursorToken) && cursorToken !== '--help' && cursorToken !== '-h') { + printCursorLegacySubcommandDeprecation(cursorToken); + const exitCode = await handleCursorCommand(args.slice(1)); + process.exit(exitCode); + } + } + // First-time install: offer setup wizard for interactive users // Check independently of recovery status (user may have empty config.yaml) // Skip if headless, CI, or non-TTY environment @@ -551,17 +584,8 @@ async function main(): Promise { try { // Detect profile (strip --target flags before profile detection) const cleanArgs = stripTargetFlag(args); - const useCursorCliproxyShortcut = shouldUseCursorCliproxyShortcut(cleanArgs); - const { profile, remainingArgs } = useCursorCliproxyShortcut - ? { profile: 'cursor', remainingArgs: cleanArgs.slice(1) } - : detectProfile(cleanArgs); - const profileInfo: ProfileDetectionResult = useCursorCliproxyShortcut - ? { - type: 'cliproxy', - name: 'cursor', - provider: 'cursor', - } - : detector.detectProfileType(profile); + const { profile, remainingArgs } = detectProfile(cleanArgs); + const profileInfo: ProfileDetectionResult = detector.detectProfileType(profile); let resolvedTarget: ReturnType; try { resolvedTarget = resolveTargetType( diff --git a/src/cliproxy/config/env-builder.ts b/src/cliproxy/config/env-builder.ts index dcace13b..f5013944 100644 --- a/src/cliproxy/config/env-builder.ts +++ b/src/cliproxy/config/env-builder.ts @@ -19,7 +19,7 @@ import { normalizeProtocol, CLIPROXY_DEFAULT_PORT, } from './port-manager'; -import { getProviderSettingsPath } from './path-resolver'; +import { migrateLegacyProviderSettingsIfNeeded } from './path-resolver'; import { canonicalizeModelIdForProvider, MODEL_ENV_VAR_KEYS, @@ -465,7 +465,7 @@ export function getEffectiveEnvVars( } // Priority 2: Default provider settings file - const settingsPath = getProviderSettingsPath(provider); + const settingsPath = migrateLegacyProviderSettingsIfNeeded(provider); // Check for user override file if (fs.existsSync(settingsPath)) { @@ -505,7 +505,7 @@ export function getEffectiveEnvVars( * Called during installation/first run */ export function ensureProviderSettings(provider: CLIProxyProvider): void { - const settingsPath = getProviderSettingsPath(provider); + const settingsPath = migrateLegacyProviderSettingsIfNeeded(provider); const defaultEnv = getClaudeEnvVars(provider); const writeSettings = (settings: Record): void => { @@ -663,7 +663,7 @@ export function getRemoteEnvVars( // Priority 2: Default provider settings file (~/.ccs/{provider}.settings.json) if (Object.keys(userEnvVars).length === 0) { - const settingsPath = getProviderSettingsPath(provider); + const settingsPath = migrateLegacyProviderSettingsIfNeeded(provider); if (fs.existsSync(settingsPath)) { try { const content = fs.readFileSync(settingsPath, 'utf-8'); diff --git a/src/cliproxy/config/path-resolver.ts b/src/cliproxy/config/path-resolver.ts index 21746fc7..413d6aee 100644 --- a/src/cliproxy/config/path-resolver.ts +++ b/src/cliproxy/config/path-resolver.ts @@ -16,6 +16,13 @@ export function getCliproxyDir(): string { return path.join(getCcsDir(), 'cliproxy'); } +/** + * Get CLIProxy provider settings directory. + */ +export function getCliproxyProvidersDir(): string { + return path.join(getCliproxyDir(), 'providers'); +} + /** * Get CLIProxy writable directory for logs and runtime files. * This directory is set as WRITABLE_PATH env var when spawning CLIProxy. @@ -75,5 +82,40 @@ export function getBinDir(): string { * Example: ~/.ccs/gemini.settings.json */ export function getProviderSettingsPath(provider: CLIProxyProvider): string { + if (provider === 'cursor') { + return path.join(getCliproxyProvidersDir(), `${provider}.settings.json`); + } + + return getLegacyProviderSettingsPath(provider); +} + +/** + * Get CLIProxy provider settings path in the dedicated cliproxy/providers namespace. + * Used only for providers that must not collide with legacy top-level settings files. + */ +export function getDedicatedProviderSettingsPath(provider: CLIProxyProvider): string { + return path.join(getCliproxyProvidersDir(), `${provider}.settings.json`); +} + +/** + * Get legacy provider settings file path in ~/.ccs root. + * This is kept for compatibility reads/migration of older provider settings. + */ +export function getLegacyProviderSettingsPath(provider: CLIProxyProvider): string { return path.join(getCcsDir(), `${provider}.settings.json`); } + +/** + * Resolve the effective provider settings path. + * + * Cursor uses a dedicated cliproxy/providers namespace so it does not collide + * with the deprecated Cursor IDE bridge raw settings file. + */ +export function migrateLegacyProviderSettingsIfNeeded(provider: CLIProxyProvider): string { + if (provider !== 'cursor') { + return getProviderSettingsPath(provider); + } + + const targetPath = getDedicatedProviderSettingsPath(provider); + return targetPath; +} diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index 21c02ef6..b1824361 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { InteractivePrompt } from '../utils/prompt'; import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog'; -import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator'; +import { getClaudeEnvVars, migrateLegacyProviderSettingsIfNeeded } from './config-generator'; import { CLIProxyProvider } from './types'; import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; import { getCcsDir } from '../utils/config-manager'; @@ -31,7 +31,7 @@ function canonicalizeModelForProvider(provider: CLIProxyProvider, model: string) * Check if provider has user settings configured */ export function hasUserSettings(provider: CLIProxyProvider): boolean { - const settingsPath = getProviderSettingsPath(provider); + const settingsPath = migrateLegacyProviderSettingsIfNeeded(provider); return fs.existsSync(settingsPath); } @@ -46,7 +46,7 @@ export function getCurrentModel( ): string | undefined { const settingsPath = customSettingsPath ? customSettingsPath.replace(/^~/, os.homedir()) - : getProviderSettingsPath(provider); + : migrateLegacyProviderSettingsIfNeeded(provider); if (!fs.existsSync(settingsPath)) return undefined; try { @@ -116,7 +116,7 @@ export async function configureProviderModel( // Use custom settings path for CLIProxy variants, otherwise use default provider path const settingsPath = customSettingsPath ? customSettingsPath.replace(/^~/, os.homedir()) - : getProviderSettingsPath(provider); + : migrateLegacyProviderSettingsIfNeeded(provider); // Skip if already configured with a model (unless --config flag). // A settings file can exist without model env keys (e.g., hook-only writes). @@ -249,7 +249,7 @@ export async function showCurrentConfig(provider: CLIProxyProvider): Promise CLIProxy -> Cursor', '', - 'Usage: ccs cursor ', + `Usage: ${LEGACY_CURSOR_COMMAND} `, '', 'Subcommands (deprecated compatibility for the local reverse-engineered bridge):', ' auth Import Cursor IDE authentication token (deprecated)', @@ -32,24 +35,24 @@ export function renderCursorHelp(): number { ' help Show this help message', '', 'Supported CLIProxy path:', - ' ccs cursor --auth # Authenticate Cursor via CLIProxy', - ' ccs cursor --accounts # Manage CLIProxy Cursor accounts', - ' ccs cursor --config # Open CLIProxy Cursor settings', + ` ${CLIPROXY_CURSOR_COMMAND} --auth # Authenticate Cursor via CLIProxy`, + ` ${CLIPROXY_CURSOR_COMMAND} --accounts # Manage CLIProxy Cursor accounts`, + ` ${CLIPROXY_CURSOR_COMMAND} --config # Open CLIProxy Cursor settings`, '', 'Legacy runtime entry (deprecated compatibility):', - ' ccs cursor [claude args] # Run Claude via the local Cursor bridge', + ` ${LEGACY_CURSOR_COMMAND} [claude args] # Run Claude via the local Cursor bridge`, '', 'Legacy auth options:', - ' ccs cursor auth # Auto-detect from Cursor SQLite (deprecated)', - ' ccs cursor auth --manual --token --machine-id ', + ` ${LEGACY_CURSOR_COMMAND} auth # Auto-detect from Cursor SQLite (deprecated)`, + ` ${LEGACY_CURSOR_COMMAND} auth --manual --token --machine-id `, '', 'Legacy bridge quick start:', - ' 1. ccs cursor enable # Deprecated compatibility: enable local bridge', - ' 2. ccs cursor auth # Deprecated compatibility: import Cursor IDE token', - ' 3. ccs cursor start # Start local daemon', - ' 4. ccs cursor probe # Verify live runtime health', - ' 5. ccs cursor "task" # Run Claude through the local bridge', - ' 6. ccs cursor status # Inspect auth/daemon wiring', + ` 1. ${LEGACY_CURSOR_COMMAND} enable # Deprecated compatibility: enable local bridge`, + ` 2. ${LEGACY_CURSOR_COMMAND} auth # Deprecated compatibility: import Cursor IDE token`, + ` 3. ${LEGACY_CURSOR_COMMAND} start # Start local daemon`, + ` 4. ${LEGACY_CURSOR_COMMAND} probe # Verify live runtime health`, + ` 5. ${LEGACY_CURSOR_COMMAND} "task" # Run Claude through the local bridge`, + ` 6. ${LEGACY_CURSOR_COMMAND} status # Inspect auth/daemon wiring`, '', 'Web UI: ccs config -> Deprecated -> Cursor IDE', '', @@ -109,11 +112,13 @@ export function renderCursorStatus( console.log(''); console.log('Client setup:'); console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`); - console.log(' Runtime entry: ccs cursor [claude args] (deprecated compatibility)'); - console.log(' Supported auth: ccs cursor --auth'); - console.log(' Live probe: ccs cursor probe'); - console.log(' Status command: ccs cursor status'); - console.log(' Help command: ccs cursor help'); + console.log( + ` Runtime entry: ${LEGACY_CURSOR_COMMAND} [claude args] (deprecated compatibility)` + ); + console.log(` Supported auth: ${CLIPROXY_CURSOR_COMMAND} --auth`); + console.log(` Live probe: ${LEGACY_CURSOR_COMMAND} probe`); + console.log(` Status command: ${LEGACY_CURSOR_COMMAND} status`); + console.log(` Help command: ${LEGACY_CURSOR_COMMAND} help`); if (isReady) { return; @@ -123,16 +128,16 @@ export function renderCursorStatus( console.log('Next steps:'); if (!cursorConfig.enabled) { - console.log(' - Enable: ccs cursor enable'); + console.log(` - Enable: ${LEGACY_CURSOR_COMMAND} enable`); } if (!authStatus.authenticated || authStatus.expired) { - console.log(' - Supported: ccs cursor --auth'); - console.log(' - Legacy auth: ccs cursor auth'); + console.log(` - Supported: ${CLIPROXY_CURSOR_COMMAND} --auth`); + console.log(` - Legacy auth: ${LEGACY_CURSOR_COMMAND} auth`); } if (!daemonStatus.running) { - console.log(' - Start: ccs cursor start'); + console.log(` - Start: ${LEGACY_CURSOR_COMMAND} start`); } - console.log(' - Help: ccs cursor help'); + console.log(` - Help: ${LEGACY_CURSOR_COMMAND} help`); } export function renderCursorModels(models: CursorModel[], defaultModel: string): void { diff --git a/src/commands/cursor-command.ts b/src/commands/cursor-command.ts index 29f5d34b..80c2f260 100644 --- a/src/commands/cursor-command.ts +++ b/src/commands/cursor-command.ts @@ -26,10 +26,13 @@ import { } from './cursor-command-display'; import { ok, fail, info } from '../utils/ui'; +const LEGACY_CURSOR_COMMAND = 'ccs legacy cursor'; +const CLIPROXY_CURSOR_COMMAND = 'ccs cursor'; + function printLegacyCursorDeprecationNotice(): void { console.log( info( - 'Deprecated compatibility path. Prefer CLIProxy-backed Cursor auth via `ccs cursor --auth` or the CLIProxy dashboard.' + `Deprecated compatibility path. \`${CLIPROXY_CURSOR_COMMAND}\` now belongs to the CLIProxy Cursor provider; use \`${LEGACY_CURSOR_COMMAND}\` for the old bridge.` ) ); console.log(''); @@ -135,7 +138,7 @@ async function handleAuth(args: string[]): Promise { if (!accessToken || !machineId) { console.error( fail( - 'Manual auth requires both token and machine ID.\n\nExample:\n ccs cursor auth --manual --token --machine-id ' + `Manual auth requires both token and machine ID.\n\nExample:\n ${LEGACY_CURSOR_COMMAND} auth --manual --token --machine-id ` ) ); return 1; @@ -156,9 +159,9 @@ async function handleAuth(args: string[]): Promise { console.log(ok('Cursor credentials imported (manual mode)')); console.log(''); console.log('Next steps:'); - console.log(' 0. Preferred auth: ccs cursor --auth'); - console.log(' 1. Enable integration: ccs cursor enable'); - console.log(' 2. Start daemon: ccs cursor start'); + console.log(` 0. Preferred auth: ${CLIPROXY_CURSOR_COMMAND} --auth`); + console.log(` 1. Enable integration: ${LEGACY_CURSOR_COMMAND} enable`); + console.log(` 2. Start daemon: ${LEGACY_CURSOR_COMMAND} start`); return 0; } @@ -179,10 +182,10 @@ async function handleAuth(args: string[]): Promise { console.log(ok('Auto-detected Cursor credentials')); console.log(''); console.log('Next steps:'); - console.log(' 0. Preferred auth: ccs cursor --auth'); - console.log(' 1. Enable integration: ccs cursor enable'); - console.log(' 2. Start daemon: ccs cursor start'); - console.log(' 3. Check status: ccs cursor status'); + console.log(` 0. Preferred auth: ${CLIPROXY_CURSOR_COMMAND} --auth`); + console.log(` 1. Enable integration: ${LEGACY_CURSOR_COMMAND} enable`); + console.log(` 2. Start daemon: ${LEGACY_CURSOR_COMMAND} start`); + console.log(` 3. Check status: ${LEGACY_CURSOR_COMMAND} status`); return 0; } @@ -190,7 +193,7 @@ async function handleAuth(args: string[]): Promise { printAutoDetectFailure(autoResult); console.log(''); console.log('Manual fallback:'); - console.log(' ccs cursor auth --manual --token --machine-id '); + console.log(` ${LEGACY_CURSOR_COMMAND} auth --manual --token --machine-id `); console.log(''); return 1; @@ -230,17 +233,17 @@ async function handleStart(): Promise { const cursorConfig = getCursorConfig(); if (!cursorConfig.enabled) { - console.error(fail('Cursor integration is disabled. Run: ccs cursor enable')); + console.error(fail(`Cursor integration is disabled. Run: ${LEGACY_CURSOR_COMMAND} enable`)); return 1; } const authStatus = checkAuthStatus(); if (!authStatus.authenticated) { - console.error(fail('Not authenticated. Run: ccs cursor auth')); + console.error(fail(`Not authenticated. Run: ${LEGACY_CURSOR_COMMAND} auth`)); return 1; } if (authStatus.expired) { - console.error(fail('Credentials expired. Run: ccs cursor auth')); + console.error(fail(`Credentials expired. Run: ${LEGACY_CURSOR_COMMAND} auth`)); return 1; } @@ -294,10 +297,10 @@ async function handleEnable(): Promise { console.log(ok('Cursor integration enabled')); console.log(''); console.log('Next steps:'); - console.log(' 0. Preferred auth: ccs cursor --auth'); - console.log(' 1. Authenticate: ccs cursor auth'); - console.log(' 2. Start daemon: ccs cursor start'); - console.log(' 3. Check status: ccs cursor status'); + console.log(` 0. Preferred auth: ${CLIPROXY_CURSOR_COMMAND} --auth`); + console.log(` 1. Authenticate: ${LEGACY_CURSOR_COMMAND} auth`); + console.log(` 2. Start daemon: ${LEGACY_CURSOR_COMMAND} start`); + console.log(` 3. Check status: ${LEGACY_CURSOR_COMMAND} status`); return 0; } diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index e02c00c9..ecdf187c 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -344,8 +344,7 @@ export async function handleHelpRoute( cliproxy: async () => (await import('./cliproxy/help-subcommand')).showHelp(), copilot: async () => process.exit(await (await import('./copilot-command')).handleCopilotCommand(['--help'])), - cursor: async () => - process.exit(await (await import('./cursor-command')).handleCursorCommand(['--help'])), + cursor: async () => await showProviderShortcutHelp('cursor', writeLine), docker: async () => (await import('./docker/help-subcommand')).showHelp(), migrate: async () => (await import('./migrate-command')).printMigrateHelp(), setup: async () => (await import('./setup-command')).handleSetupCommand(['--help']), diff --git a/src/config/reserved-names.ts b/src/config/reserved-names.ts index 75d0c924..b6ca1c5f 100644 --- a/src/config/reserved-names.ts +++ b/src/config/reserved-names.ts @@ -20,6 +20,8 @@ export const RESERVED_PROFILE_NAMES = [ 'copilot', // Cursor IDE (Cursor proxy daemon) 'cursor', + 'legacy-cursor', + 'legacy', // CLI commands and special names 'default', 'config', diff --git a/src/cursor/constants.ts b/src/cursor/constants.ts index 2f1d3c99..b8e88d36 100644 --- a/src/cursor/constants.ts +++ b/src/cursor/constants.ts @@ -1,3 +1,5 @@ +export const LEGACY_CURSOR_PROFILE_NAME = 'legacy-cursor'; + export const CURSOR_SUBCOMMANDS = [ 'auth', 'status', diff --git a/src/cursor/cursor-daemon-entry.ts b/src/cursor/cursor-daemon-entry.ts index bfcea61c..868121da 100644 --- a/src/cursor/cursor-daemon-entry.ts +++ b/src/cursor/cursor-daemon-entry.ts @@ -248,7 +248,7 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser const authStatus = checkAuthStatus(); if (!authStatus.authenticated || !authStatus.credentials) { - const message = 'Cursor credentials not found. Run `ccs cursor auth` first.'; + const message = 'Cursor credentials not found. Run `ccs legacy cursor auth` first.'; if (isAnthropicRoute) { await pipeWebResponseToNode( createAnthropicErrorResponse(401, 'authentication_error', message), @@ -266,7 +266,7 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser } if (authStatus.expired) { - const message = 'Cursor credentials expired. Run `ccs cursor auth` again.'; + const message = 'Cursor credentials expired. Run `ccs legacy cursor auth` again.'; if (isAnthropicRoute) { await pipeWebResponseToNode( createAnthropicErrorResponse(401, 'authentication_error', message), diff --git a/src/cursor/cursor-profile-executor.ts b/src/cursor/cursor-profile-executor.ts index 74900445..1a1b239f 100644 --- a/src/cursor/cursor-profile-executor.ts +++ b/src/cursor/cursor-profile-executor.ts @@ -97,7 +97,7 @@ export async function executeCursorProfile( if (!config.enabled) { console.error(fail('Cursor integration is not enabled.')); console.error(''); - console.error('Enable it first: ccs cursor enable'); + console.error('Enable it first: ccs legacy cursor enable'); return 1; } @@ -105,13 +105,13 @@ export async function executeCursorProfile( if (!authStatus.authenticated) { console.error(fail('Cursor credentials not found.')); console.error(''); - console.error('Authenticate first: ccs cursor auth'); + console.error('Authenticate first: ccs legacy cursor auth'); return 1; } if (authStatus.expired) { console.error(fail('Cursor credentials have expired.')); console.error(''); - console.error('Refresh them with: ccs cursor auth'); + console.error('Refresh them with: ccs legacy cursor auth'); return 1; } @@ -133,7 +133,7 @@ export async function executeCursorProfile( console.error(fail('Cursor daemon is not running.')); console.error(''); console.error('Start the daemon:'); - console.error(' ccs cursor start'); + console.error(' ccs legacy cursor start'); console.error('Or enable auto_start in the Cursor config section.'); return 1; } diff --git a/src/cursor/cursor-runtime-probe.ts b/src/cursor/cursor-runtime-probe.ts index 3dadfa36..302bbb75 100644 --- a/src/cursor/cursor-runtime-probe.ts +++ b/src/cursor/cursor-runtime-probe.ts @@ -120,7 +120,7 @@ export async function probeCursorRuntime(config: CursorConfig): Promise { }; expect(anthropicBody.type).toBe('error'); expect(anthropicBody.error?.type).toBe('authentication_error'); - expect(anthropicBody.error?.message).toContain('Run `ccs cursor auth` first'); + expect(anthropicBody.error?.message).toContain('Run `ccs legacy cursor auth` first'); const stopResult = await stopDaemon(); expect(stopResult.success).toBe(true); diff --git a/tests/npm/cli.test.js b/tests/npm/cli.test.js index e31a2654..78448948 100644 --- a/tests/npm/cli.test.js +++ b/tests/npm/cli.test.js @@ -87,17 +87,22 @@ describe('npm CLI', () => { }); it('routes cursor probe through the cursor command handler', function() { + let output = ''; try { - execSync(`bun "${srcCcsPath}" cursor probe`, { + output = execSync(`bun "${srcCcsPath}" cursor probe`, { + encoding: 'utf8', stdio: 'pipe', timeout: 3000, env: { ...process.env, CCS_HOME: testCcsHome } }); } catch (e) { - const output = e.stderr?.toString() || e.stdout?.toString() || ''; - assert(!output.includes("Profile 'cursor' not found"), 'Should not fall through to profile lookup'); - assert(output.includes('Cursor Live Probe'), 'Should render the cursor probe command output'); + output = e.stderr?.toString() || e.stdout?.toString() || ''; } + assert(!output.includes("Profile 'cursor' not found"), 'Should not fall through to profile lookup'); + assert( + output.includes('Cursor Live Probe') || output.includes('legacy cursor probe'), + 'Should route through the legacy cursor compatibility handler' + ); }); it('routes gitlab --help to provider shortcut help instead of starting auth', function() { diff --git a/tests/unit/auth/profile-detector.test.ts b/tests/unit/auth/profile-detector.test.ts index 145697cb..6c8293c4 100644 --- a/tests/unit/auth/profile-detector.test.ts +++ b/tests/unit/auth/profile-detector.test.ts @@ -202,7 +202,14 @@ describe('ProfileDetector', () => { } }); - it('should detect cursor as a first-class runtime profile when enabled', () => { + it('should detect cursor as a CLIProxy provider shortcut', () => { + const result = detector.detectProfileType('cursor'); + expect(result.type).toBe('cliproxy'); + expect(result.name).toBe('cursor'); + expect(result.provider).toBe('cursor'); + }); + + it('should detect legacy-cursor as a first-class runtime profile when enabled', () => { const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true); const getCursorConfigSpy = spyOn(unifiedConfigLoader, 'getCursorConfig').mockReturnValue({ enabled: true, @@ -213,9 +220,9 @@ describe('ProfileDetector', () => { }); try { - const result = detector.detectProfileType('cursor'); + const result = detector.detectProfileType('legacy-cursor'); expect(result.type).toBe('cursor'); - expect(result.name).toBe('cursor'); + expect(result.name).toBe('legacy-cursor'); expect(result.cursorConfig?.auto_start).toBe(true); } finally { isUnifiedModeSpy.mockRestore(); @@ -223,7 +230,7 @@ describe('ProfileDetector', () => { } }); - it('should merge default cursor fields when enabled via partial unified config', () => { + it('should merge default legacy cursor fields when enabled via partial unified config', () => { const originalCcsHome = process.env.CCS_HOME; process.env.CCS_HOME = tempDir; const ccsDir = path.join(tempDir, '.ccs'); @@ -235,7 +242,7 @@ describe('ProfileDetector', () => { try { const localDetector = new ProfileDetector(); - const result = localDetector.detectProfileType('cursor'); + const result = localDetector.detectProfileType('legacy-cursor'); expect(result.type).toBe('cursor'); expect(result.cursorConfig).toEqual({ enabled: true, @@ -253,7 +260,7 @@ describe('ProfileDetector', () => { } }); - it('should throw a helpful error when cursor profile is disabled', () => { + it('should throw a helpful error when legacy cursor profile is disabled', () => { const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true); const getCursorConfigSpy = spyOn(unifiedConfigLoader, 'getCursorConfig').mockReturnValue({ enabled: false, @@ -264,7 +271,9 @@ describe('ProfileDetector', () => { }); try { - expect(() => detector.detectProfileType('cursor')).toThrow(/Cursor profile is not enabled/); + expect(() => detector.detectProfileType('legacy-cursor')).toThrow( + /Legacy Cursor profile is not enabled/ + ); } finally { isUnifiedModeSpy.mockRestore(); getCursorConfigSpy.mockRestore(); diff --git a/tests/unit/commands/completion-backend.test.ts b/tests/unit/commands/completion-backend.test.ts index 7b278719..60d3a814 100644 --- a/tests/unit/commands/completion-backend.test.ts +++ b/tests/unit/commands/completion-backend.test.ts @@ -140,6 +140,15 @@ describe('completion backend', () => { expect(suggestionValues(['cliproxy'])).toEqual(expect.arrayContaining(['remove', '--backend'])); }); + test('treats cursor as a provider shortcut in completion', () => { + const values = suggestionValues(['cursor']); + expect(values).toEqual( + expect.arrayContaining(['--auth', '--accounts', '--config', '--logout']) + ); + expect(values).not.toContain('probe'); + expect(values).not.toContain('start'); + }); + test('filters suggestions by the current token prefix', () => { const values = suggestionValues([], 'do'); expect(values).toEqual(expect.arrayContaining(['docker', 'doctor'])); diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index 18b70d16..edfac3bd 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -322,7 +322,9 @@ describe('handleCursorCommand', () => { expect(exitCode).toBe(0); expect(errors).toHaveLength(0); expect(logs.some((line) => line.includes('Legacy Cursor Compatibility'))).toBe(true); - expect(logs.some((line) => line.includes('Usage: ccs cursor '))).toBe(true); + expect(logs.some((line) => line.includes('Usage: ccs legacy cursor '))).toBe( + true + ); } finally { console.log = originalLog; console.error = originalError; @@ -423,7 +425,9 @@ describe('renderCursorStatus', () => { true ); expect(logs.some((line) => line.includes('Next steps:'))).toBe(true); - expect(logs.some((line) => line.includes(' - Help: ccs cursor help'))).toBe(true); + expect(logs.some((line) => line.includes(' - Help: ccs legacy cursor help'))).toBe( + true + ); } finally { console.log = originalLog; } @@ -483,11 +487,15 @@ describe('renderCursorHelp', () => { const exitCode = renderCursorHelp(); expect(exitCode).toBe(0); - expect(logs.some((line) => line.includes('Usage: ccs cursor '))).toBe(true); - expect(logs.some((line) => line.includes('Legacy Cursor Compatibility'))).toBe(true); - expect(logs.some((line) => line.includes('Deprecated: prefer CLIProxy-backed Cursor auth'))).toBe( + expect(logs.some((line) => line.includes('Usage: ccs legacy cursor '))).toBe( true ); + expect(logs.some((line) => line.includes('Legacy Cursor Compatibility'))).toBe(true); + expect( + logs.some((line) => + line.includes('Deprecated: `ccs cursor` now belongs to the CLIProxy Cursor provider.') + ) + ).toBe(true); expect(logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))).toBe( true ); @@ -495,7 +503,7 @@ describe('renderCursorHelp', () => { logs.some((line) => line.includes('ccs cursor --auth')) ).toBe(true); expect( - logs.some((line) => line.includes('ccs cursor [claude args]')) + logs.some((line) => line.includes('ccs legacy cursor [claude args]')) ).toBe(true); } finally { console.log = originalLog; diff --git a/tests/unit/utils/hooks/image-analysis-backend-resolver.test.ts b/tests/unit/utils/hooks/image-analysis-backend-resolver.test.ts index 7525e7d3..246a0d3f 100644 --- a/tests/unit/utils/hooks/image-analysis-backend-resolver.test.ts +++ b/tests/unit/utils/hooks/image-analysis-backend-resolver.test.ts @@ -82,6 +82,21 @@ describe('image-analysis-backend-resolver', () => { expect(status.resolutionSource).toBe('profile-backend'); }); + it('treats cliproxy cursor as a provider-backed profile rather than a legacy bridge alias', () => { + const status = resolveImageAnalysisStatus( + { + profileName: 'cursor', + profileType: 'cliproxy', + cliproxyProvider: 'cursor', + }, + DEFAULT_IMAGE_ANALYSIS_CONFIG + ); + + expect(status.backendId).toBe('cursor'); + expect(status.resolutionSource).toBe('cliproxy-provider'); + expect(status.reason).toContain('no image-analysis model configured'); + }); + it('uses the fallback backend for an unmapped third-party settings profile', () => { const status = resolveImageAnalysisStatus( { diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 16dc92ed..1ebba72f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; import { queryClient } from '@/lib/query-client'; @@ -129,6 +129,10 @@ export default function App() { /> } + /> + }> diff --git a/ui/src/components/cliproxy/provider-editor/provider-editor-header.tsx b/ui/src/components/cliproxy/provider-editor/provider-editor-header.tsx index dc251b61..69c4100b 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-editor-header.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-editor-header.tsx @@ -25,6 +25,17 @@ interface ProviderEditorHeaderProps { onSave: () => void; } +function formatSettingsPathBadgeLabel(pathValue: string): string { + const normalized = pathValue.replace(/\\/g, '/'); + const cliproxySegment = '/cliproxy/providers/'; + const cliproxyIndex = normalized.lastIndexOf(cliproxySegment); + if (cliproxyIndex !== -1) { + return normalized.slice(cliproxyIndex + 1); + } + + return normalized.replace(/^.*\//, ''); +} + export function ProviderEditorHeader({ displayName, logoProvider, @@ -63,7 +74,7 @@ export function ProviderEditorHeader({ )} {!isRemoteMode && data?.path && ( - {data.path.replace(/^.*[\\/]/, '')} + {formatSettingsPathBadgeLabel(data.path)} )} diff --git a/ui/src/components/cliproxy/provider-editor/use-provider-editor.ts b/ui/src/components/cliproxy/provider-editor/use-provider-editor.ts index 8412ec44..739955c1 100644 --- a/ui/src/components/cliproxy/provider-editor/use-provider-editor.ts +++ b/ui/src/components/cliproxy/provider-editor/use-provider-editor.ts @@ -15,6 +15,7 @@ import { isAnthropicModelEnvKey, } from '@/lib/extended-context-utils'; import { supportsExtendedContext } from '@/lib/model-catalogs'; +import { isValidProvider } from '@/lib/provider-config'; /** Required env vars for CLIProxy providers (informational only - runtime fills defaults) */ const REQUIRED_ENV_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN'] as const; @@ -39,12 +40,18 @@ export function useProviderEditor( queryFn: async () => { const res = await fetch(`/api/settings/${provider}/raw`); if (!res.ok) { + const fallbackPath = + provider === 'cursor' + ? `~/.ccs/cliproxy/providers/${provider}.settings.json` + : isValidProvider(provider) + ? `~/.ccs/${provider}.settings.json` + : `~/.ccs/profiles/${provider}/settings.json`; // Return empty settings for unconfigured providers return { profile: provider, settings: { env: {} }, mtime: Date.now(), - path: `~/.ccs/profiles/${provider}/settings.json`, + path: fallbackPath, }; } return res.json(); diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx index 1d9c01e0..a50ee9e4 100644 --- a/ui/src/components/layout/app-sidebar.tsx +++ b/ui/src/components/layout/app-sidebar.tsx @@ -126,7 +126,11 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] { { title: t('nav.deprecated'), items: [ - { path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') }, + { + path: '/legacy/cursor', + iconSrc: '/assets/sidebar/cursor.svg', + label: `${t('nav.cursorIde')} (Legacy)`, + }, ], }, { diff --git a/ui/src/hooks/use-cursor.ts b/ui/src/hooks/use-cursor.ts index 0b67bf3c..3ece6d0c 100644 --- a/ui/src/hooks/use-cursor.ts +++ b/ui/src/hooks/use-cursor.ts @@ -57,6 +57,8 @@ interface CursorAuthResult { message: string; } +const LEGACY_CURSOR_API_BASE = '/legacy/cursor'; + export interface CursorProbeResult { ok: boolean; stage: 'config' | 'auth' | 'daemon' | 'runtime'; @@ -87,25 +89,25 @@ function getProbeErrorMessage(value: unknown): string | null { } async function fetchCursorStatus(): Promise { - const res = await fetch(withApiBase('/cursor/status')); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/status`)); if (!res.ok) throw new Error('Failed to fetch cursor status'); return res.json(); } async function fetchCursorConfig(): Promise { - const res = await fetch(withApiBase('/cursor/settings')); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings`)); if (!res.ok) throw new Error('Failed to fetch cursor config'); return res.json(); } async function fetchCursorModels(): Promise { - const res = await fetch(withApiBase('/cursor/models')); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/models`)); if (!res.ok) throw new Error('Failed to fetch cursor models'); return res.json(); } async function fetchCursorRawSettings(): Promise { - const res = await fetch(withApiBase('/cursor/settings/raw')); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings/raw`)); if (!res.ok) throw new Error('Failed to fetch cursor raw settings'); return res.json(); } @@ -113,7 +115,7 @@ async function fetchCursorRawSettings(): Promise { async function updateCursorConfig( updates: Partial ): Promise<{ success: boolean; cursor: CursorConfig }> { - const res = await fetch(withApiBase('/cursor/settings'), { + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), @@ -126,7 +128,7 @@ async function saveCursorRawSettings(data: { settings: CursorRawSettings['settings']; expectedMtime?: number; }): Promise<{ success: boolean; mtime: number }> { - const res = await fetch(withApiBase('/cursor/settings/raw'), { + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings/raw`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -137,7 +139,9 @@ async function saveCursorRawSettings(data: { } async function autoDetectCursorAuth(): Promise { - const res = await fetch(withApiBase('/cursor/auth/auto-detect'), { method: 'POST' }); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/auth/auto-detect`), { + method: 'POST', + }); if (!res.ok) { const error = await res.json().catch(() => ({ error: 'Auto-detect failed' })); throw new Error(error.error || 'Auto-detect failed'); @@ -149,7 +153,7 @@ async function importCursorAuthManual(data: { accessToken: string; machineId: string; }): Promise { - const res = await fetch(withApiBase('/cursor/auth/import'), { + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/auth/import`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -162,19 +166,23 @@ async function importCursorAuthManual(data: { } async function startCursorDaemon(): Promise<{ success: boolean; pid?: number; error?: string }> { - const res = await fetch(withApiBase('/cursor/daemon/start'), { method: 'POST' }); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/daemon/start`), { + method: 'POST', + }); if (!res.ok) throw new Error('Failed to start cursor daemon'); return res.json(); } async function stopCursorDaemon(): Promise<{ success: boolean; error?: string }> { - const res = await fetch(withApiBase('/cursor/daemon/stop'), { method: 'POST' }); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/daemon/stop`), { + method: 'POST', + }); if (!res.ok) throw new Error('Failed to stop cursor daemon'); return res.json(); } async function probeCursorRuntime(): Promise { - const res = await fetch(withApiBase('/cursor/probe'), { method: 'POST' }); + const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/probe`), { method: 'POST' }); const payload = await res.json().catch(() => null); if (isCursorProbeResult(payload)) { diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index 1767e3ad..989c393c 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -1283,7 +1283,7 @@ export function CursorPage() { {t('cursorPage.provider')} - Cursor IDE + Cursor IDE (Legacy)
@@ -1295,7 +1295,7 @@ export function CursorPage() {
{/* TODO i18n: missing key for model mapping env var info paragraph */}

- Model mapping writes `ANTHROPIC_MODEL`, + Legacy bridge model mapping writes `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`.

diff --git a/ui/tests/unit/hooks/use-cursor.test.tsx b/ui/tests/unit/hooks/use-cursor.test.tsx index 7b4f947f..849de3fb 100644 --- a/ui/tests/unit/hooks/use-cursor.test.tsx +++ b/ui/tests/unit/hooks/use-cursor.test.tsx @@ -32,7 +32,7 @@ describe('useCursor', () => { const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); - if (url.endsWith('/api/cursor/status')) { + if (url.endsWith('/api/legacy/cursor/status')) { return Promise.resolve( createJsonResponse({ enabled: true, @@ -48,7 +48,7 @@ describe('useCursor', () => { ); } - if (url.endsWith('/api/cursor/settings')) { + if (url.endsWith('/api/legacy/cursor/settings')) { return Promise.resolve( createJsonResponse({ enabled: true, @@ -60,7 +60,7 @@ describe('useCursor', () => { ); } - if (url.endsWith('/api/cursor/models')) { + if (url.endsWith('/api/legacy/cursor/models')) { return Promise.resolve( createJsonResponse({ models: [{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'openai' }], @@ -69,7 +69,7 @@ describe('useCursor', () => { ); } - if (url.endsWith('/api/cursor/settings/raw')) { + if (url.endsWith('/api/legacy/cursor/settings/raw')) { return Promise.resolve( createJsonResponse({ settings: {}, @@ -80,7 +80,7 @@ describe('useCursor', () => { ); } - if (url.endsWith('/api/cursor/probe') && init?.method === 'POST') { + if (url.endsWith('/api/legacy/cursor/probe') && init?.method === 'POST') { return Promise.resolve(createJsonResponse(probeFailure, 503)); } @@ -102,14 +102,16 @@ describe('useCursor', () => { await waitFor(() => expect(result.current.probeResult).toMatchObject(probeFailure)); await waitFor(() => expect( - fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/status')) - .length + fetchMock.mock.calls.filter(([input]) => + String(input).endsWith('/api/legacy/cursor/status') + ).length ).toBeGreaterThanOrEqual(2) ); await waitFor(() => expect( - fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/models')) - .length + fetchMock.mock.calls.filter(([input]) => + String(input).endsWith('/api/legacy/cursor/models') + ).length ).toBeGreaterThanOrEqual(2) ); });