cursor-keys 1.0.0 → 1.2.0
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/README.md +43 -21
- package/bin/cursor-keys.mjs +14 -5
- package/extension/LICENSE +21 -0
- package/package.json +1 -2
- package/src/commands.mjs +67 -7
- package/src/config.mjs +37 -13
- package/src/detect.mjs +22 -0
- package/src/profile.mjs +11 -5
- package/extension/cursor-keys-1.0.0.vsix +0 -0
package/README.md
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```
|
|
4
|
+
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██╗ ██╗███████╗██╗ ██╗███████╗
|
|
5
|
+
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝
|
|
6
|
+
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ █████╔╝ █████╗ ╚████╔╝ ███████╗
|
|
7
|
+
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ██╔═██╗ ██╔══╝ ╚██╔╝ ╚════██║
|
|
8
|
+
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ██║ ██╗███████╗ ██║ ███████║
|
|
9
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝
|
|
10
|
+
```
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
**Per-project Cursor profiles so every window uses the right BYOK API key.**
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
[](#development)
|
|
17
|
+
|
|
18
|
+
[Website](https://l-ubu.github.io/cursor-keys/) · [Install](#install) · [Commands](#commands) · [Config](#config)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
11
23
|
|
|
24
|
+
Cursor stores your OpenAI-compatible API key (LiteLLM, Vercel AI Gateway, corporate proxy, ...) **once per profile**, not per repo. If you bill five client projects, you either babysit Settings all day or bill the wrong project. `cursor-keys` gives each project its own isolated profile and makes opening, switching, and checking keys automatic.
|
|
25
|
+
|
|
26
|
+
```
|
|
12
27
|
$ cursor-keys status
|
|
13
28
|
|
|
14
|
-
Project
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
Project | Color | Windows | Key | Repo
|
|
30
|
+
----------+----------+---------+-----+------------------
|
|
31
|
+
acme | #4f8cff | 1 | yes | ~/Projects/acme
|
|
32
|
+
client-a | #51cf66 | 0 | no | ~/Projects/client-a
|
|
33
|
+
client-b | #845ef7 | 1 | yes | ~/Projects/client-b
|
|
19
34
|
|
|
20
|
-
$ cursor-keys doctor
|
|
35
|
+
$ cursor-keys doctor acme
|
|
21
36
|
|
|
22
|
-
✓ OpenAI
|
|
37
|
+
✓ OpenAI API key row present
|
|
23
38
|
✓ Explore subagent overrides present
|
|
24
|
-
✓ Base URL (
|
|
39
|
+
✓ Base URL (gateway.example.com/v1) in profile state
|
|
25
40
|
```
|
|
26
41
|
|
|
27
42
|
## Install
|
|
@@ -36,8 +51,8 @@ Requires Node ≥18, the Cursor CLI (`Shell Command: Install 'cursor' in PATH`),
|
|
|
36
51
|
## Quick start
|
|
37
52
|
|
|
38
53
|
```bash
|
|
39
|
-
# one-time
|
|
40
|
-
cursor-keys config baseUrl https://
|
|
54
|
+
# one-time: point at your OpenAI-compatible gateway
|
|
55
|
+
cursor-keys config baseUrl https://gateway.example.com/v1
|
|
41
56
|
|
|
42
57
|
# one-time per project
|
|
43
58
|
cursor-keys add acme --path ~/Projects/acme # register + create profile
|
|
@@ -64,7 +79,7 @@ One window, one profile, one key. Each project gets an isolated `--user-data-dir
|
|
|
64
79
|
|
|
65
80
|
Settings flow one way, main to profiles: `sync` re-copies your main `settings.json` into every profile and re-applies that profile's title bar color on top. `open` does this automatically before every launch, and open windows hot-reload the file, so tweaks in your main Cursor show up everywhere. Changes made inside a profile window are overwritten on the next sync.
|
|
66
81
|
|
|
67
|
-
`clone` copies your main Cursor state (SQLite backup) so profiles open signed-in with your theme and model config. `set-key` encrypts your project key with Cursor's own safeStorage (Chromium OSCrypt, key from the "Cursor Safe Storage" Keychain item) and writes it into the profile's secret store, round-trip verified. macOS asks for Keychain permission once; click Always Allow.
|
|
82
|
+
`clone` copies your main Cursor state (SQLite backup) so profiles open signed-in with your theme and model config. `set-key` encrypts your project key with Cursor's own safeStorage (Chromium OSCrypt, key from the "Cursor Safe Storage" macOS Keychain item) and writes it into the profile's secret store, round-trip verified. macOS asks for Keychain permission once; click Always Allow.
|
|
68
83
|
|
|
69
84
|
## "Which key am I on?" in three layers
|
|
70
85
|
|
|
@@ -83,7 +98,7 @@ Settings flow one way, main to profiles: `sync` re-copies your main `settings.js
|
|
|
83
98
|
| `open [name]` | Launch Cursor with the profile (auto-detect from cwd) |
|
|
84
99
|
| `status` | Projects, colors, running windows, key configured |
|
|
85
100
|
| `current` | Mapping for the current directory |
|
|
86
|
-
| `config [baseUrl [URL]]`
|
|
101
|
+
| `config [get <key> · baseUrl [URL]]` | Show summary, print one value, or set the gateway base URL |
|
|
87
102
|
| `sync [name]` | Re-copy main Cursor settings into profiles |
|
|
88
103
|
| `doctor [name]` | Health checks: key row, base URL, subagent overrides |
|
|
89
104
|
| `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
|
|
@@ -98,14 +113,21 @@ Settings flow one way, main to profiles: `sync` re-copies your main `settings.js
|
|
|
98
113
|
```json
|
|
99
114
|
{
|
|
100
115
|
"profilesRoot": "~/.cursor-profiles",
|
|
101
|
-
"baseUrl": "https://
|
|
116
|
+
"baseUrl": "https://gateway.example.com/v1",
|
|
102
117
|
"projects": {
|
|
103
118
|
"acme": { "path": "~/Projects/acme", "color": "#4f8cff" }
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
121
|
```
|
|
107
122
|
|
|
108
|
-
`baseUrl` is your gateway's OpenAI-compatible endpoint
|
|
123
|
+
`baseUrl` is your gateway's OpenAI-compatible endpoint. `doctor` verifies it per profile.
|
|
124
|
+
|
|
125
|
+
Read a single value for scripts (stdout only, exit 1 on unknown keys):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
cursor-keys config get baseUrl
|
|
129
|
+
cursor-keys config get profilesRoot
|
|
130
|
+
```
|
|
109
131
|
|
|
110
132
|
## Security
|
|
111
133
|
|
package/bin/cursor-keys.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { runCommand } from "../src/commands.mjs";
|
|
4
|
-
import { bold,
|
|
4
|
+
import { bold, dim, reset, yellow } from "../src/ansi.mjs";
|
|
5
5
|
|
|
6
6
|
const BANNER = `
|
|
7
|
-
${
|
|
8
|
-
${
|
|
9
|
-
${
|
|
7
|
+
${yellow} ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██╗ ██╗███████╗██╗ ██╗███████╗
|
|
8
|
+
${yellow}██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝
|
|
9
|
+
${yellow}██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ █████╔╝ █████╗ ╚████╔╝ ███████╗
|
|
10
|
+
${yellow}██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ██╔═██╗ ██╔══╝ ╚██╔╝ ╚════██║
|
|
11
|
+
${yellow}╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ██║ ██╗███████╗ ██║ ███████║
|
|
12
|
+
${yellow} ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝${reset}${dim} per-project Cursor API keys
|
|
10
13
|
`;
|
|
11
14
|
|
|
12
15
|
function printHelp() {
|
|
@@ -17,6 +20,12 @@ function printHelp() {
|
|
|
17
20
|
" init [--root DIR] [--all] Scan for git repos and register projects",
|
|
18
21
|
);
|
|
19
22
|
console.log(" add <name> [options] Create profile for a project");
|
|
23
|
+
console.log(
|
|
24
|
+
" clone <name> [--from main] Copy sign-in + settings from a donor profile",
|
|
25
|
+
);
|
|
26
|
+
console.log(
|
|
27
|
+
" set-key <name> Inject the project API key (encrypted)",
|
|
28
|
+
);
|
|
20
29
|
console.log(
|
|
21
30
|
" open [name] Open Cursor with this project's profile",
|
|
22
31
|
);
|
|
@@ -25,7 +34,7 @@ function printHelp() {
|
|
|
25
34
|
);
|
|
26
35
|
console.log(" current Show mapping for cwd");
|
|
27
36
|
console.log(
|
|
28
|
-
" config [baseUrl [URL]]
|
|
37
|
+
" config [get <key>|baseUrl [URL]] Show, read, or set config values",
|
|
29
38
|
);
|
|
30
39
|
console.log(
|
|
31
40
|
" sync [name] Re-copy main Cursor settings into profiles",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 L-ubu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-keys",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Per-project Cursor profiles so each window uses the correct BYOK API key. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"cursor",
|
|
20
20
|
"cli",
|
|
21
21
|
"api-key",
|
|
22
|
-
"bonzai",
|
|
23
22
|
"byok",
|
|
24
23
|
"developer-tools",
|
|
25
24
|
"productivity"
|
package/src/commands.mjs
CHANGED
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
baseUrlFragment,
|
|
9
9
|
collapseHome,
|
|
10
10
|
CONFIG_FILE,
|
|
11
|
+
CONFIG_GETTABLE_KEYS,
|
|
11
12
|
expandHome,
|
|
12
13
|
findProjectByPath,
|
|
14
|
+
getConfigValue,
|
|
13
15
|
getProject,
|
|
14
16
|
loadConfig,
|
|
15
17
|
pickColorForName,
|
|
18
|
+
pickUnusedColor,
|
|
16
19
|
saveConfig,
|
|
17
20
|
profileStateDb,
|
|
18
21
|
} from "./config.mjs";
|
|
@@ -32,8 +35,10 @@ import {
|
|
|
32
35
|
findCursorCli,
|
|
33
36
|
listRunningCursorProfiles,
|
|
34
37
|
mainCursorProfileRoot,
|
|
38
|
+
mainCursorUserDir,
|
|
35
39
|
stateDbHasKey,
|
|
36
40
|
stateDbValueContains,
|
|
41
|
+
stateDbValuesMatch,
|
|
37
42
|
hasSqlite3,
|
|
38
43
|
} from "./detect.mjs";
|
|
39
44
|
import {
|
|
@@ -174,12 +179,30 @@ function ask(question) {
|
|
|
174
179
|
async function cmdConfig(args) {
|
|
175
180
|
const { flags, positional } = parseFlags(args);
|
|
176
181
|
if (flags.help) {
|
|
177
|
-
console.log(
|
|
182
|
+
console.log(
|
|
183
|
+
"Usage: cursor-keys config Show config summary",
|
|
184
|
+
);
|
|
185
|
+
console.log(
|
|
186
|
+
" cursor-keys config get <key> Print one value (baseUrl, profilesRoot)",
|
|
187
|
+
);
|
|
188
|
+
console.log(
|
|
189
|
+
" cursor-keys config baseUrl <URL> Set gateway base URL",
|
|
190
|
+
);
|
|
178
191
|
return;
|
|
179
192
|
}
|
|
180
193
|
const config = loadConfig();
|
|
181
194
|
const [key, value] = positional;
|
|
182
195
|
|
|
196
|
+
if (key === "get") {
|
|
197
|
+
if (!value) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Missing config key. Usage: cursor-keys config get <${CONFIG_GETTABLE_KEYS.join("|")}>`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
console.log(getConfigValue(config, value));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
if (!key) {
|
|
184
207
|
const count = Object.keys(config.projects).length;
|
|
185
208
|
console.log(
|
|
@@ -292,10 +315,17 @@ async function cmdInit(args) {
|
|
|
292
315
|
}
|
|
293
316
|
saveConfig(config);
|
|
294
317
|
console.log(
|
|
295
|
-
`\nNext: cursor-keys add <name> --clone main (creates profile + copies
|
|
318
|
+
`\nNext: cursor-keys add <name> --clone main (creates profile + copies main Cursor settings)`,
|
|
296
319
|
);
|
|
297
320
|
}
|
|
298
321
|
|
|
322
|
+
function shouldShowFirstRunChecklist(profileDir) {
|
|
323
|
+
if (!needsFirstRunChecklist(profileDir)) return false;
|
|
324
|
+
const db = profileStateDb(profileDir);
|
|
325
|
+
// A cloned profile already carries a key row: setup is effectively done.
|
|
326
|
+
return !(fs.existsSync(db) && stateDbHasKey(db, SECRET_OPENAI_KEY));
|
|
327
|
+
}
|
|
328
|
+
|
|
299
329
|
async function cmdAdd(args) {
|
|
300
330
|
const { flags, positional } = parseFlags(args);
|
|
301
331
|
if (flags.help) {
|
|
@@ -319,8 +349,13 @@ async function cmdAdd(args) {
|
|
|
319
349
|
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
320
350
|
}
|
|
321
351
|
|
|
352
|
+
const usedColors = Object.entries(config.projects)
|
|
353
|
+
.filter(([other]) => other !== name)
|
|
354
|
+
.map(([, entry]) => entry.color);
|
|
322
355
|
const color =
|
|
323
|
-
flags.color ||
|
|
356
|
+
flags.color ||
|
|
357
|
+
config.projects[name]?.color ||
|
|
358
|
+
pickUnusedColor(name, usedColors);
|
|
324
359
|
config.projects[name] = { path: collapseHome(repoPath), color };
|
|
325
360
|
saveConfig(config);
|
|
326
361
|
|
|
@@ -348,7 +383,7 @@ async function cmdAdd(args) {
|
|
|
348
383
|
console.log(ok(`Profile ready: ${collapseHome(project.profileDir)}`));
|
|
349
384
|
console.log(` Repo: ${collapseHome(project.path)}`);
|
|
350
385
|
console.log(` Color: ${color} ${blockSwatch(color)}`);
|
|
351
|
-
if (
|
|
386
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
352
387
|
printFirstRunChecklist(name, config.baseUrl);
|
|
353
388
|
}
|
|
354
389
|
}
|
|
@@ -405,7 +440,7 @@ async function cmdOpen(args) {
|
|
|
405
440
|
console.log(` ${dim}${collapseHome(project.path)}${reset}`);
|
|
406
441
|
console.log(` Profile ${blockSwatch(project.color)} ${project.color}`);
|
|
407
442
|
|
|
408
|
-
if (
|
|
443
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
409
444
|
printFirstRunChecklist(name, config.baseUrl);
|
|
410
445
|
markSetupDone(project.profileDir);
|
|
411
446
|
}
|
|
@@ -499,9 +534,22 @@ async function cmdDoctor(args) {
|
|
|
499
534
|
}
|
|
500
535
|
const db = profileStateDb(p.profileDir);
|
|
501
536
|
if (fs.existsSync(db)) {
|
|
537
|
+
const hasKey = stateDbHasKey(db, SECRET_OPENAI_KEY);
|
|
502
538
|
console.log(
|
|
503
|
-
` ${
|
|
539
|
+
` ${hasKey ? ok("✓") : warn("✗")} OpenAI API key row present`,
|
|
504
540
|
);
|
|
541
|
+
if (hasKey) {
|
|
542
|
+
const mainDb = path.join(
|
|
543
|
+
mainCursorUserDir(),
|
|
544
|
+
"globalStorage",
|
|
545
|
+
"state.vscdb",
|
|
546
|
+
);
|
|
547
|
+
if (stateDbValuesMatch(db, mainDb, SECRET_OPENAI_KEY)) {
|
|
548
|
+
console.log(
|
|
549
|
+
` ${warn("!")} Key is identical to the main profile (inherited via clone); run: cursor-keys set-key ${name}`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
505
553
|
console.log(
|
|
506
554
|
` ${stateDbHasKey(db, SUBAGENT_OVERRIDES_KEY) ? ok("✓") : warn("✗")} Explore subagent overrides present`,
|
|
507
555
|
);
|
|
@@ -747,7 +795,19 @@ async function cmdSetKey(args) {
|
|
|
747
795
|
throw new Error(`Close the "${name}" Cursor window first, then retry.`);
|
|
748
796
|
}
|
|
749
797
|
|
|
750
|
-
|
|
798
|
+
let key = flags.key;
|
|
799
|
+
if (!key) {
|
|
800
|
+
console.log(
|
|
801
|
+
`\nPaste the API key for ${bold}${name}${reset} below, then press Enter.`,
|
|
802
|
+
);
|
|
803
|
+
console.log(
|
|
804
|
+
`${dim}Input is hidden: nothing echoes while you paste or type.${reset}`,
|
|
805
|
+
);
|
|
806
|
+
key = await promptHidden("API key: ");
|
|
807
|
+
}
|
|
808
|
+
if (!key) {
|
|
809
|
+
throw new Error("No key entered. Aborted, nothing was written.");
|
|
810
|
+
}
|
|
751
811
|
const result = setProfileApiKey(db, key);
|
|
752
812
|
console.log(
|
|
753
813
|
ok(`Key set for ${name}`) +
|
package/src/config.mjs
CHANGED
|
@@ -98,25 +98,49 @@ export function baseUrlFragment(config) {
|
|
|
98
98
|
return String(config.baseUrl || "").replace(/^https?:\/\//, "");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export const CONFIG_GETTABLE_KEYS = ["baseUrl", "profilesRoot"];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {ReturnType<typeof loadConfig>} config
|
|
105
|
+
* @param {string} key
|
|
106
|
+
*/
|
|
107
|
+
export function getConfigValue(config, key) {
|
|
108
|
+
if (!CONFIG_GETTABLE_KEYS.includes(key)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Unknown config key: ${key}. Known keys: ${CONFIG_GETTABLE_KEYS.join(", ")}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return config[key];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const COLOR_PALETTE = [
|
|
117
|
+
"#4f8cff",
|
|
118
|
+
"#ff6b6b",
|
|
119
|
+
"#51cf66",
|
|
120
|
+
"#ffd43b",
|
|
121
|
+
"#845ef7",
|
|
122
|
+
"#20c997",
|
|
123
|
+
"#ff922b",
|
|
124
|
+
"#e64980",
|
|
125
|
+
"#339af0",
|
|
126
|
+
"#94d82d",
|
|
127
|
+
];
|
|
128
|
+
|
|
101
129
|
/** Deterministic palette color from project name */
|
|
102
130
|
export function pickColorForName(name) {
|
|
103
|
-
const palette = [
|
|
104
|
-
"#4f8cff",
|
|
105
|
-
"#ff6b6b",
|
|
106
|
-
"#51cf66",
|
|
107
|
-
"#ffd43b",
|
|
108
|
-
"#845ef7",
|
|
109
|
-
"#20c997",
|
|
110
|
-
"#ff922b",
|
|
111
|
-
"#e64980",
|
|
112
|
-
"#339af0",
|
|
113
|
-
"#94d82d",
|
|
114
|
-
];
|
|
115
131
|
let hash = 0;
|
|
116
132
|
for (let i = 0; i < name.length; i++) {
|
|
117
133
|
hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
|
|
118
134
|
}
|
|
119
|
-
return
|
|
135
|
+
return COLOR_PALETTE[hash % COLOR_PALETTE.length];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Like pickColorForName, but avoids colors already used by other projects */
|
|
139
|
+
export function pickUnusedColor(name, usedColors) {
|
|
140
|
+
const used = new Set(usedColors);
|
|
141
|
+
const preferred = pickColorForName(name);
|
|
142
|
+
if (!used.has(preferred)) return preferred;
|
|
143
|
+
return COLOR_PALETTE.find((c) => !used.has(c)) || preferred;
|
|
120
144
|
}
|
|
121
145
|
|
|
122
146
|
/**
|
package/src/detect.mjs
CHANGED
|
@@ -183,6 +183,28 @@ export function stateDbValueContains(stateDbPath, urlFragment) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* True if the value for `key` is byte-identical in both state DBs.
|
|
188
|
+
* Comparison happens inside SQL; the value never enters this process.
|
|
189
|
+
* @param {string} dbA
|
|
190
|
+
* @param {string} dbB
|
|
191
|
+
* @param {string} key
|
|
192
|
+
*/
|
|
193
|
+
export function stateDbValuesMatch(dbA, dbB, key) {
|
|
194
|
+
if (!fs.existsSync(dbA) || !fs.existsSync(dbB) || !hasSqlite3()) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const out = execSync(
|
|
199
|
+
`sqlite3 ${shellQuote(dbA)} "ATTACH DATABASE ${shellQuote(dbB)} AS other; SELECT CASE WHEN (SELECT value FROM ItemTable WHERE key = ${shellQuote(key)}) IS NOT NULL AND (SELECT value FROM ItemTable WHERE key = ${shellQuote(key)}) = (SELECT value FROM other.ItemTable WHERE key = ${shellQuote(key)}) THEN 1 ELSE 0 END;"`,
|
|
200
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] },
|
|
201
|
+
);
|
|
202
|
+
return out.trim() === "1";
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
186
208
|
function shellQuote(s) {
|
|
187
209
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
188
210
|
}
|
package/src/profile.mjs
CHANGED
|
@@ -116,7 +116,14 @@ export function cloneStateDb(donorProfileDir, targetProfileDir) {
|
|
|
116
116
|
ensureProfileStructure(targetProfileDir);
|
|
117
117
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
// VACUUM INTO requires a fresh destination.
|
|
120
|
+
for (const suffix of ["", "-wal", "-shm", "-journal"]) {
|
|
121
|
+
fs.rmSync(dest + suffix, { force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// VACUUM INTO snapshots a consistent copy even while the donor Cursor is
|
|
125
|
+
// running; `.backup` can spin at full CPU against a write-heavy donor.
|
|
126
|
+
execSync(`sqlite3 ${shellQuote(src)} "VACUUM INTO ${shellQuote(dest)}"`, {
|
|
120
127
|
stdio: "ignore",
|
|
121
128
|
});
|
|
122
129
|
}
|
|
@@ -138,17 +145,16 @@ export function needsFirstRunChecklist(profileDir) {
|
|
|
138
145
|
|
|
139
146
|
export function printFirstRunChecklist(
|
|
140
147
|
projectName,
|
|
141
|
-
baseUrl = "https://
|
|
148
|
+
baseUrl = "https://gateway.example.com/v1",
|
|
142
149
|
) {
|
|
143
150
|
console.log(`
|
|
144
151
|
First-time setup for "${projectName}" in this Cursor profile:
|
|
145
152
|
|
|
146
153
|
1. Cursor Settings → Models → API keys
|
|
147
|
-
2. Paste your project
|
|
154
|
+
2. Paste your project API key under OpenAI API Key (or run: cursor-keys set-key ${projectName})
|
|
148
155
|
3. Enable "Use OpenAI API Key" and "Override OpenAI Base URL"
|
|
149
156
|
4. Base URL: ${baseUrl}
|
|
150
|
-
5.
|
|
151
|
-
6. Add custom Bonzai models and disable non-Bonzai models
|
|
157
|
+
5. Add your gateway models and disable models you do not use
|
|
152
158
|
|
|
153
159
|
Then run: cursor-keys doctor ${projectName}
|
|
154
160
|
`);
|
|
Binary file
|