cursor-keys 1.0.0 → 1.1.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.
@@ -17,6 +17,12 @@ function printHelp() {
17
17
  " init [--root DIR] [--all] Scan for git repos and register projects",
18
18
  );
19
19
  console.log(" add <name> [options] Create profile for a project");
20
+ console.log(
21
+ " clone <name> [--from main] Copy sign-in + settings from a donor profile",
22
+ );
23
+ console.log(
24
+ " set-key <name> Inject the project API key (encrypted)",
25
+ );
20
26
  console.log(
21
27
  " open [name] Open Cursor with this project's profile",
22
28
  );
@@ -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.0.0",
3
+ "version": "1.1.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": {
package/src/commands.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  getProject,
14
14
  loadConfig,
15
15
  pickColorForName,
16
+ pickUnusedColor,
16
17
  saveConfig,
17
18
  profileStateDb,
18
19
  } from "./config.mjs";
@@ -32,8 +33,10 @@ import {
32
33
  findCursorCli,
33
34
  listRunningCursorProfiles,
34
35
  mainCursorProfileRoot,
36
+ mainCursorUserDir,
35
37
  stateDbHasKey,
36
38
  stateDbValueContains,
39
+ stateDbValuesMatch,
37
40
  hasSqlite3,
38
41
  } from "./detect.mjs";
39
42
  import {
@@ -296,6 +299,13 @@ async function cmdInit(args) {
296
299
  );
297
300
  }
298
301
 
302
+ function shouldShowFirstRunChecklist(profileDir) {
303
+ if (!needsFirstRunChecklist(profileDir)) return false;
304
+ const db = profileStateDb(profileDir);
305
+ // A cloned profile already carries a key row: setup is effectively done.
306
+ return !(fs.existsSync(db) && stateDbHasKey(db, SECRET_OPENAI_KEY));
307
+ }
308
+
299
309
  async function cmdAdd(args) {
300
310
  const { flags, positional } = parseFlags(args);
301
311
  if (flags.help) {
@@ -319,8 +329,13 @@ async function cmdAdd(args) {
319
329
  throw new Error(`Repo path does not exist: ${repoPath}`);
320
330
  }
321
331
 
332
+ const usedColors = Object.entries(config.projects)
333
+ .filter(([other]) => other !== name)
334
+ .map(([, entry]) => entry.color);
322
335
  const color =
323
- flags.color || config.projects[name]?.color || pickColorForName(name);
336
+ flags.color ||
337
+ config.projects[name]?.color ||
338
+ pickUnusedColor(name, usedColors);
324
339
  config.projects[name] = { path: collapseHome(repoPath), color };
325
340
  saveConfig(config);
326
341
 
@@ -348,7 +363,7 @@ async function cmdAdd(args) {
348
363
  console.log(ok(`Profile ready: ${collapseHome(project.profileDir)}`));
349
364
  console.log(` Repo: ${collapseHome(project.path)}`);
350
365
  console.log(` Color: ${color} ${blockSwatch(color)}`);
351
- if (needsFirstRunChecklist(project.profileDir)) {
366
+ if (shouldShowFirstRunChecklist(project.profileDir)) {
352
367
  printFirstRunChecklist(name, config.baseUrl);
353
368
  }
354
369
  }
@@ -405,7 +420,7 @@ async function cmdOpen(args) {
405
420
  console.log(` ${dim}${collapseHome(project.path)}${reset}`);
406
421
  console.log(` Profile ${blockSwatch(project.color)} ${project.color}`);
407
422
 
408
- if (needsFirstRunChecklist(project.profileDir)) {
423
+ if (shouldShowFirstRunChecklist(project.profileDir)) {
409
424
  printFirstRunChecklist(name, config.baseUrl);
410
425
  markSetupDone(project.profileDir);
411
426
  }
@@ -499,9 +514,22 @@ async function cmdDoctor(args) {
499
514
  }
500
515
  const db = profileStateDb(p.profileDir);
501
516
  if (fs.existsSync(db)) {
517
+ const hasKey = stateDbHasKey(db, SECRET_OPENAI_KEY);
502
518
  console.log(
503
- ` ${stateDbHasKey(db, SECRET_OPENAI_KEY) ? ok("✓") : warn("✗")} OpenAI/Bonzai API key row present`,
519
+ ` ${hasKey ? ok("✓") : warn("✗")} OpenAI/Bonzai API key row present`,
504
520
  );
521
+ if (hasKey) {
522
+ const mainDb = path.join(
523
+ mainCursorUserDir(),
524
+ "globalStorage",
525
+ "state.vscdb",
526
+ );
527
+ if (stateDbValuesMatch(db, mainDb, SECRET_OPENAI_KEY)) {
528
+ console.log(
529
+ ` ${warn("!")} Key is identical to the main profile (inherited via clone); run: cursor-keys set-key ${name}`,
530
+ );
531
+ }
532
+ }
505
533
  console.log(
506
534
  ` ${stateDbHasKey(db, SUBAGENT_OVERRIDES_KEY) ? ok("✓") : warn("✗")} Explore subagent overrides present`,
507
535
  );
@@ -747,7 +775,19 @@ async function cmdSetKey(args) {
747
775
  throw new Error(`Close the "${name}" Cursor window first, then retry.`);
748
776
  }
749
777
 
750
- const key = flags.key || (await promptHidden(`API key for ${name}: `));
778
+ let key = flags.key;
779
+ if (!key) {
780
+ console.log(
781
+ `\nPaste the API key for ${bold}${name}${reset} below, then press Enter.`,
782
+ );
783
+ console.log(
784
+ `${dim}Input is hidden: nothing echoes while you paste or type.${reset}`,
785
+ );
786
+ key = await promptHidden("API key: ");
787
+ }
788
+ if (!key) {
789
+ throw new Error("No key entered. Aborted, nothing was written.");
790
+ }
751
791
  const result = setProfileApiKey(db, key);
752
792
  console.log(
753
793
  ok(`Key set for ${name}`) +
package/src/config.mjs CHANGED
@@ -98,25 +98,34 @@ export function baseUrlFragment(config) {
98
98
  return String(config.baseUrl || "").replace(/^https?:\/\//, "");
99
99
  }
100
100
 
101
+ const COLOR_PALETTE = [
102
+ "#4f8cff",
103
+ "#ff6b6b",
104
+ "#51cf66",
105
+ "#ffd43b",
106
+ "#845ef7",
107
+ "#20c997",
108
+ "#ff922b",
109
+ "#e64980",
110
+ "#339af0",
111
+ "#94d82d",
112
+ ];
113
+
101
114
  /** Deterministic palette color from project name */
102
115
  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
116
  let hash = 0;
116
117
  for (let i = 0; i < name.length; i++) {
117
118
  hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
118
119
  }
119
- return palette[hash % palette.length];
120
+ return COLOR_PALETTE[hash % COLOR_PALETTE.length];
121
+ }
122
+
123
+ /** Like pickColorForName, but avoids colors already used by other projects */
124
+ export function pickUnusedColor(name, usedColors) {
125
+ const used = new Set(usedColors);
126
+ const preferred = pickColorForName(name);
127
+ if (!used.has(preferred)) return preferred;
128
+ return COLOR_PALETTE.find((c) => !used.has(c)) || preferred;
120
129
  }
121
130
 
122
131
  /**
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
- execSync(`sqlite3 ${shellQuote(src)} ".backup ${shellQuote(dest)}"`, {
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
  }
Binary file