cursor-keys 1.2.3 → 1.3.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 CHANGED
@@ -48,7 +48,29 @@ npm install -g cursor-keys
48
48
 
49
49
  Requires Node ≥18, the Cursor CLI (`Shell Command: Install 'cursor' in PATH`), and sqlite3 (preinstalled on macOS). Key injection is macOS-only; everything else is cross-platform shaped.
50
50
 
51
- On global install, the companion Cursor extension is linked into `~/.cursor/extensions` automatically and a one-time hint prints (opt out of both: `CURSOR_KEYS_NO_POSTINSTALL=1`). Restart Cursor, then `Cmd+Shift+P` → "cursor-keys: Open Project...".
51
+ The package runs **no install scripts** (`ignore-scripts=true` safe). Link the companion extension once yourself:
52
+
53
+ ```bash
54
+ cursor-keys install-extension # or install "cursor-keys" from the Open VSX marketplace in Cursor
55
+ ```
56
+
57
+ Restart Cursor, then `Cmd+Shift+P` → "cursor-keys: Open Project...".
58
+
59
+ ## No terminal? Let Cursor's agent set it up
60
+
61
+ Open your project in Cursor, paste this into the agent chat (`Cmd+L`), and it runs the whole setup for you. Your API key goes into a hidden terminal prompt, never into the chat:
62
+
63
+ ```text
64
+ Set up cursor-keys (github.com/L-ubu/cursor-keys) for this project, step by step:
65
+ 1. Run `npm install -g cursor-keys`.
66
+ 2. Run `cursor-keys config baseUrl <your-gateway-url>` (replace with my gateway URL, e.g. https://gateway.example.com/v1).
67
+ 3. Register this workspace: `cursor-keys add <folder-name> --path <this workspace's path>`.
68
+ 4. Run `cursor-keys clone <folder-name>` to inherit my Cursor sign-in and settings.
69
+ 5. Run `cursor-keys set-key <folder-name>` and ask me to paste the project API key at the hidden terminal prompt. Never write the key into files, chat, or logs.
70
+ 6. Run `cursor-keys doctor <folder-name>` and fix what it flags.
71
+ 7. Run `cursor-keys aliases --install` and `cursor-keys wrap --install` for the shell extras.
72
+ 8. Open the project in its profile with `cursor-keys open <folder-name>`, then explain how I can tell it worked.
73
+ ```
52
74
 
53
75
  ## Quick start
54
76
 
@@ -72,7 +94,7 @@ One-time extras for the full experience:
72
94
  ```bash
73
95
  cursor-keys wrap --install # plain `cursor <path>` auto-picks the profile
74
96
  cursor-keys aliases --install # per-project aliases, `ck` shortcut, zsh tab completion
75
- cursor-keys install-extension # status bar + wrong-window guard (auto-linked on global npm install)
97
+ cursor-keys install-extension # status bar + wrong-window guard
76
98
  ```
77
99
 
78
100
  ## How it works
@@ -81,7 +103,7 @@ One window, one profile, one key. Each project gets an isolated `--user-data-dir
81
103
 
82
104
  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.
83
105
 
84
- `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.
106
+ `clone` copies your main Cursor state (SQLite backup) so profiles open signed-in with your theme and model config, then scrubs donor secrets (`secret://` rows, MCP OAuth tokens, extension secrets) out of the copy. The sign-in session itself is the one thing that does carry over, see [Security](#security). `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. When macOS asks for Keychain permission, click **Allow**: one run, one prompt. Always Allow also works, but it permanently grants the calling binary access to that Keychain item, which is the same pattern infostealers abuse against browser safe storage.
85
107
 
86
108
  ## "Which key am I on?" in three layers
87
109
 
@@ -91,23 +113,24 @@ Settings flow one way, main to profiles: `sync` re-copies your main `settings.js
91
113
 
92
114
  ## Commands
93
115
 
94
- | Command | What it does |
95
- | ---------------------------------------------- | ------------------------------------------------------------ |
96
- | `init [--root DIR] [--all]` | Discover git repos and register them |
97
- | `add <name> [--path] [--color] [--clone main]` | Create a project profile |
98
- | `clone <name> [--from main]` | Copy sign-in + settings + model config from a donor profile |
99
- | `set-key <name> [--key sk-...]` | Inject the project API key (encrypted, verified) |
100
- | `open [name]` | Launch Cursor with the profile (auto-detect from cwd) |
101
- | `status` | Projects, colors, running windows, key configured |
102
- | `current` | Mapping for the current directory |
103
- | `config [get <key> · baseUrl [URL]]` | Show summary, print one value, or set the gateway base URL |
104
- | `sync [name]` | Re-copy main Cursor settings into profiles |
105
- | `doctor [name]` | Health checks: key row, base URL, subagent overrides |
106
- | `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
116
+ | Command | What it does |
117
+ | ---------------------------------------------- | --------------------------------------------------------------- |
118
+ | `init [--root DIR] [--all]` | Discover git repos and register them |
119
+ | `add <name> [--path] [--color] [--clone main]` | Create a project profile |
120
+ | `clone <name> [--from main]` | Copy sign-in + settings + model config from a donor profile |
121
+ | `set-key <name>` | Inject the project API key (hidden prompt or stdin, encrypted) |
122
+ | `color <name> [#hex]` | Show or set the project title bar color |
123
+ | `open [name]` | Launch Cursor with the profile (auto-detect from cwd) |
124
+ | `status` | Projects, colors, running windows, key configured |
125
+ | `current` | Mapping for the current directory |
126
+ | `config [get <key> · baseUrl [URL]]` | Show summary, print one value, or set the gateway base URL |
127
+ | `sync [name]` | Re-copy main Cursor settings into profiles |
128
+ | `doctor [name]` | Health checks: key row, base URL, subagent overrides |
129
+ | `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
107
130
  | `aliases [--install]` | Per-project shell aliases (also `ck` shortcut + tab completion) |
108
- | `completion` | Print zsh tab completion (auto-sourced by `aliases --install`) |
109
- | `install-extension` | Link the Cursor extension (auto-linked on global npm install) |
110
- | `rm <name> [--delete]` | Unregister (optionally delete the profile) |
131
+ | `completion` | Print zsh tab completion (auto-sourced by `aliases --install`) |
132
+ | `install-extension` | Link the Cursor extension (status bar + wrong-window guard) |
133
+ | `rm <name> [--delete]` | Unregister (optionally delete the profile) |
111
134
 
112
135
  `cursor-keys ls` prints registered project names one per line for scripts and tab completion; it is hidden from `--help`.
113
136
 
@@ -136,9 +159,13 @@ cursor-keys config get profilesRoot
136
159
 
137
160
  ## Security
138
161
 
139
- - Keys go **straight into Cursor's own encrypted store** (`secret://cursorAuth/openAIKey`), encrypted with the same safeStorage mechanism Cursor uses. Never in config files, logs, or stdout.
140
- - `set-key` reads the "Cursor Safe Storage" Keychain item to encrypt, the same key Cursor itself uses. Nothing leaves your machine.
141
- - `clone` copies `state.vscdb` locally via SQLite backup. The donor can keep running; the target profile window must be closed.
162
+ Full threat model in [SECURITY.md](SECURITY.md). The short version:
163
+
164
+ - Keys go **straight into Cursor's own encrypted store** (`secret://cursorAuth/openAIKey`), encrypted with the same safeStorage mechanism Cursor uses. Never in config files, logs, or stdout. `set-key` takes the key from a hidden prompt or stdin, so it never appears in shell history or a process list.
165
+ - `set-key` reads the "Cursor Safe Storage" Keychain item to encrypt, the same key Cursor itself uses. Nothing leaves your machine. Click **Allow** at the prompt; Always Allow permanently ACLs the calling binary to that item.
166
+ - `clone` copies `state.vscdb` locally via SQLite backup, then **scrubs donor secrets from the copy**: all `secret://` rows (the donor's API key, extension secrets) and MCP OAuth tokens are deleted. MCP servers re-auth once per profile.
167
+ - What clone deliberately keeps is your Cursor **sign-in session** (so profiles open signed-in). That means N profiles hold N+1 copies of the session tokens, stored exactly as Cursor stores them. Delete profiles you no longer use: `cursor-keys rm <name> --delete`.
168
+ - Supply chain: zero runtime dependencies, **no install scripts**, and releases are published from GitHub Actions with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) over OIDC trusted publishing (no long-lived npm token).
142
169
 
143
170
  ## Known limitations
144
171
 
@@ -27,6 +27,9 @@ function printHelp() {
27
27
  console.log(
28
28
  " set-key <name> Inject the project API key (encrypted)",
29
29
  );
30
+ console.log(
31
+ " color <name> [#hex] Show or set the project title bar color",
32
+ );
30
33
  console.log(
31
34
  " open [name] Open Cursor with this project's profile",
32
35
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-keys",
3
- "version": "1.2.3",
3
+ "version": "1.3.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": {
@@ -13,8 +13,7 @@
13
13
  "files": [
14
14
  "bin",
15
15
  "src",
16
- "extension",
17
- "scripts"
16
+ "extension"
18
17
  ],
19
18
  "keywords": [
20
19
  "cursor",
@@ -32,7 +31,6 @@
32
31
  },
33
32
  "scripts": {
34
33
  "test": "node --test",
35
- "postinstall": "node scripts/postinstall.mjs",
36
34
  "prepublishOnly": "npm test"
37
35
  }
38
36
  }
package/src/commands.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  findProjectByPath,
14
14
  getConfigValue,
15
15
  getProject,
16
+ isHexColor,
16
17
  loadConfig,
17
18
  pickColorForName,
18
19
  pickUnusedColor,
@@ -89,6 +90,8 @@ export async function runCommand(argv) {
89
90
  return cmdInstallExtension(rest);
90
91
  case "set-key":
91
92
  return cmdSetKey(rest);
93
+ case "color":
94
+ return cmdColor(rest);
92
95
  case "clone":
93
96
  return cmdClone(rest);
94
97
  case "rm":
@@ -212,7 +215,9 @@ async function cmdConfig(args) {
212
215
  console.log(
213
216
  `${bold}Config${reset} ${dim}(${collapseHome(CONFIG_FILE)})${reset}`,
214
217
  );
215
- console.log(` baseUrl: ${config.baseUrl}`);
218
+ console.log(
219
+ ` baseUrl: ${config.baseUrl || `${dim}(not set: cursor-keys config baseUrl <url>)${reset}`}`,
220
+ );
216
221
  console.log(` profilesRoot: ${config.profilesRoot}`);
217
222
  console.log(
218
223
  ` projects: ${count} registered ${dim}(see cursor-keys status)${reset}`,
@@ -379,7 +384,7 @@ async function cmdAdd(args) {
379
384
  }
380
385
  }
381
386
 
382
- createProfile({
387
+ const { scrubbed } = createProfile({
383
388
  profileDir: project.profileDir,
384
389
  projectName: name,
385
390
  color,
@@ -389,6 +394,11 @@ async function cmdAdd(args) {
389
394
  console.log(ok(`Profile ready: ${collapseHome(project.profileDir)}`));
390
395
  console.log(` Repo: ${collapseHome(project.path)}`);
391
396
  console.log(` Color: ${color} ${blockSwatch(color)}`);
397
+ if (cloneFrom) {
398
+ console.log(
399
+ ` ${dim}Scrubbed ${scrubbed} donor secret row(s) from the clone; sign-in retained.${reset}`,
400
+ );
401
+ }
392
402
  if (shouldShowFirstRunChecklist(project.profileDir)) {
393
403
  printFirstRunChecklist(name, config.baseUrl);
394
404
  }
@@ -480,6 +490,26 @@ async function cmdStatus() {
480
490
  });
481
491
 
482
492
  printTable(["Project", "Color", "Windows", "Key", "Repo"], rows);
493
+ if (!companionExtensionLinked()) {
494
+ console.log(
495
+ `\n${dim}Companion extension not linked: cursor-keys install-extension (status bar + wrong-window guard)${reset}`,
496
+ );
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Best-effort check that the companion extension is linked into Cursor.
502
+ * Looks for the lubu.cursor-keys-* folder the extension manifest produces.
503
+ */
504
+ function companionExtensionLinked() {
505
+ try {
506
+ const extDir = path.join(os.homedir(), ".cursor", "extensions");
507
+ return fs
508
+ .readdirSync(extDir)
509
+ .some((d) => d.startsWith("lubu.cursor-keys"));
510
+ } catch {
511
+ return false;
512
+ }
483
513
  }
484
514
 
485
515
  async function cmdCurrent() {
@@ -559,9 +589,16 @@ async function cmdDoctor(args) {
559
589
  console.log(
560
590
  ` ${stateDbHasKey(db, SUBAGENT_OVERRIDES_KEY) ? ok("✓") : warn("✗")} Explore subagent overrides present`,
561
591
  );
562
- console.log(
563
- ` ${stateDbValueContains(db, baseUrlFragment(config)) ? ok("✓") : warn("✗")} Base URL (${baseUrlFragment(config)}) in profile state`,
564
- );
592
+ const fragment = baseUrlFragment(config);
593
+ if (fragment) {
594
+ console.log(
595
+ ` ${stateDbValueContains(db, fragment) ? ok("✓") : warn("✗")} Base URL (${fragment}) in profile state`,
596
+ );
597
+ } else {
598
+ console.log(
599
+ ` ${dim}- baseUrl not set, skipping gateway check: cursor-keys config baseUrl <url>${reset}`,
600
+ );
601
+ }
565
602
  } else {
566
603
  console.log(` ${warn("✗")} Open profile once: cursor-keys open ${name}`);
567
604
  }
@@ -637,7 +674,7 @@ function shellEscapeName(name) {
637
674
  }
638
675
 
639
676
  const COMPLETION_COMMANDS =
640
- "init add clone set-key open status current config sync doctor aliases wrap install-extension completion ls rm";
677
+ "init add clone set-key color open status current config sync doctor aliases wrap install-extension completion ls rm";
641
678
 
642
679
  /**
643
680
  * Pure builder for the zsh completion script. Sourced via
@@ -648,7 +685,7 @@ export function completionScript() {
648
685
  return `#compdef cursor-keys ckeys ck
649
686
  _cursor_keys() {
650
687
  local -a projects
651
- if (( CURRENT == 3 )) && [[ \${words[2]} == (open|clone|set-key|doctor|sync|rm) ]]; then
688
+ if (( CURRENT == 3 )) && [[ \${words[2]} == (open|clone|set-key|color|doctor|sync|rm) ]]; then
652
689
  projects=(\${(f)"$(cursor-keys ls 2>/dev/null)"})
653
690
  _describe 'project' projects
654
691
  else
@@ -811,10 +848,13 @@ async function cmdClone(args) {
811
848
  donorRoot = donor.profileDir;
812
849
  }
813
850
 
814
- cloneStateDb(donorRoot, project.profileDir);
851
+ const scrubbed = cloneStateDb(donorRoot, project.profileDir);
815
852
  console.log(ok(`Cloned state from ${from} into ${name}`));
816
853
  console.log(
817
- `${dim}Sign-in, settings, and model config carried over. Now set the project key:${reset}`,
854
+ `${dim}Scrubbed ${scrubbed} donor secret row(s) from the copy (API keys, MCP OAuth, extension secrets); sign-in retained.${reset}`,
855
+ );
856
+ console.log(
857
+ `${dim}Settings and model config carried over. Now set the project key:${reset}`,
818
858
  );
819
859
  console.log(` cursor-keys set-key ${name}`);
820
860
  }
@@ -843,6 +883,47 @@ async function cmdInstallExtension() {
843
883
  console.log(`Look for ${bold}$(key) <project>${reset} in the status bar.`);
844
884
  }
845
885
 
886
+ async function cmdColor(args) {
887
+ const { flags, positional } = parseFlags(args);
888
+ if (flags.help) {
889
+ console.log("Usage: cursor-keys color <name> [#RRGGBB]");
890
+ console.log("Show or set the project's title bar color.");
891
+ return;
892
+ }
893
+ const name = positional[0];
894
+ if (!name) throw new Error("Usage: cursor-keys color <name> [#RRGGBB]");
895
+ const config = loadConfig();
896
+ const project = getProject(config, name);
897
+ if (!project) throw new Error(`Unknown project: ${name}. Run cursor-keys init or add.`);
898
+
899
+ const hex = positional[1];
900
+ if (!hex) {
901
+ console.log(`${blockSwatch(project.color)} ${project.color}`);
902
+ return;
903
+ }
904
+ if (!isHexColor(hex)) {
905
+ throw new Error(`Invalid color: ${hex} (expected #RRGGBB, e.g. #852e00)`);
906
+ }
907
+
908
+ updateConfig((c) => {
909
+ if (!c.projects[name]) throw new Error(`Unknown project: ${name}`);
910
+ c.projects[name].color = hex;
911
+ });
912
+ if (fs.existsSync(project.profileDir)) {
913
+ writeProfileSettings(project.profileDir, name, hex);
914
+ }
915
+ console.log(ok(`Color for ${name}: `) + `${blockSwatch(hex)} ${hex}`);
916
+ console.log(`${dim}Open Cursor windows reload settings automatically.${reset}`);
917
+ }
918
+
919
+ /**
920
+ * Read the API key from piped stdin. Only called when stdin is not an
921
+ * interactive TTY; interactive runs get the hidden prompt instead.
922
+ */
923
+ function readStdinKey() {
924
+ return fs.readFileSync(0, "utf8").trim();
925
+ }
926
+
846
927
  function promptHidden(question) {
847
928
  return new Promise((resolve) => {
848
929
  const rl = readline.createInterface({
@@ -863,18 +944,24 @@ function promptHidden(question) {
863
944
  async function cmdSetKey(args) {
864
945
  const { flags, positional } = parseFlags(args);
865
946
  if (flags.help) {
866
- console.log("Usage: cursor-keys set-key <name> [--key sk-...]");
947
+ console.log("Usage: cursor-keys set-key <name>");
867
948
  console.log(
868
949
  "Writes the key into the profile's encrypted store (Cursor must",
869
950
  );
870
951
  console.log(
871
- "not have that profile window open). Prompts hidden if --key omitted.",
952
+ "not have that profile window open). Hidden prompt when interactive;",
872
953
  );
954
+ console.log("reads stdin when piped. No inline key flag exists:");
955
+ console.log("shell history and process lists are not secret-safe.");
873
956
  return;
874
957
  }
875
958
  const name = positional[0];
876
- if (!name)
877
- throw new Error("Usage: cursor-keys set-key <name> [--key sk-...]");
959
+ if (!name) throw new Error("Usage: cursor-keys set-key <name>");
960
+ if (flags.key !== undefined) {
961
+ throw new Error(
962
+ `Inline --key is not accepted: shell history and process lists are not secret-safe. Use the hidden prompt or pipe stdin instead: op read op://vault/item/password | cursor-keys set-key ${name}`,
963
+ );
964
+ }
878
965
 
879
966
  const config = loadConfig();
880
967
  const project = getProject(config, name);
@@ -890,8 +977,10 @@ async function cmdSetKey(args) {
890
977
  throw new Error(`Close the "${name}" Cursor window first, then retry.`);
891
978
  }
892
979
 
893
- let key = flags.key;
894
- if (!key) {
980
+ let key;
981
+ if (!process.stdin.isTTY) {
982
+ key = readStdinKey();
983
+ } else {
895
984
  console.log(
896
985
  `\nPaste the API key for ${bold}${name}${reset} below, then press Enter.`,
897
986
  );
package/src/config.mjs CHANGED
@@ -24,6 +24,11 @@ export const DEFAULT_PROFILES_ROOT = path.join(
24
24
 
25
25
  const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
26
26
 
27
+ /** @param {unknown} s */
28
+ export function isHexColor(s) {
29
+ return typeof s === "string" && HEX_COLOR.test(s);
30
+ }
31
+
27
32
  /** @param {string} p */
28
33
  export function expandHome(p) {
29
34
  if (!p || typeof p !== "string") return p;
@@ -43,7 +48,9 @@ export function collapseHome(p) {
43
48
  export function defaultConfig() {
44
49
  return {
45
50
  profilesRoot: collapseHome(DEFAULT_PROFILES_ROOT),
46
- baseUrl: "https://connect.iobonzai.com/cursor",
51
+ // No built-in gateway URL: every install sets its own via
52
+ // `cursor-keys config baseUrl <url>`.
53
+ baseUrl: "",
47
54
  projects: {},
48
55
  };
49
56
  }
@@ -92,10 +99,7 @@ export function validateConfig(data) {
92
99
  typeof data.profilesRoot === "string"
93
100
  ? data.profilesRoot
94
101
  : collapseHome(DEFAULT_PROFILES_ROOT);
95
- const baseUrl =
96
- typeof data.baseUrl === "string" && data.baseUrl
97
- ? data.baseUrl
98
- : "https://connect.iobonzai.com/cursor";
102
+ const baseUrl = typeof data.baseUrl === "string" ? data.baseUrl : "";
99
103
  const projects = {};
100
104
  if (data.projects && typeof data.projects === "object") {
101
105
  for (const [name, entry] of Object.entries(data.projects)) {
@@ -115,7 +119,7 @@ export function validateConfig(data) {
115
119
  return { profilesRoot, baseUrl, projects };
116
120
  }
117
121
 
118
- /** "https://connect.iobonzai.com/cursor" -> "connect.iobonzai.com/cursor" */
122
+ /** "https://gateway.example.com/v1" -> "gateway.example.com/v1" */
119
123
  export function baseUrlFragment(config) {
120
124
  return String(config.baseUrl || "").replace(/^https?:\/\//, "");
121
125
  }
package/src/detect.mjs CHANGED
@@ -166,7 +166,8 @@ export function stateDbKeysLike(stateDbPath, likePattern) {
166
166
  }
167
167
 
168
168
  /**
169
- * Check if Bonzai base URL appears in any ItemTable value (substring, no key dump)
169
+ * Check if a URL fragment (e.g. the configured gateway base URL) appears in
170
+ * any ItemTable value (substring, no key dump)
170
171
  * @param {string} stateDbPath
171
172
  * @param {string} urlFragment
172
173
  */
@@ -211,4 +212,3 @@ function shellQuote(s) {
211
212
 
212
213
  export const SECRET_OPENAI_KEY = "secret://cursorAuth/openAIKey";
213
214
  export const SUBAGENT_OVERRIDES_KEY = "cursor/subagentModelOverrides";
214
- export const BONZAI_BASE_URL_FRAGMENT = "connect.iobonzai.com/cursor";
package/src/profile.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync, execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
@@ -80,9 +80,18 @@ export function symlinkSharedUserFiles(profileDir) {
80
80
  }
81
81
  }
82
82
 
83
+ /**
84
+ * Row patterns scrubbed from every cloned state DB. Donor secrets (API keys,
85
+ * MCP OAuth tokens, extension secrets) never propagate into project profiles.
86
+ * The Cursor sign-in session (cursorAuth/*) intentionally stays: profiles
87
+ * open signed-in, which is the whole point of clone.
88
+ */
89
+ export const CLONE_SCRUB_LIKE_PATTERNS = ["secret://%", "mcpOAuth.%"];
90
+
83
91
  /**
84
92
  * @param {string} donorProfileDir - main Cursor or another profile root (parent of User)
85
93
  * @param {string} targetProfileDir
94
+ * @returns {number} how many donor secret rows were scrubbed from the copy
86
95
  */
87
96
  export function cloneStateDb(donorProfileDir, targetProfileDir) {
88
97
  const runningProfiles = listRunningCursorProfiles();
@@ -126,6 +135,29 @@ export function cloneStateDb(donorProfileDir, targetProfileDir) {
126
135
  execSync(`sqlite3 ${shellQuote(src)} "VACUUM INTO ${shellQuote(dest)}"`, {
127
136
  stdio: "ignore",
128
137
  });
138
+
139
+ return scrubClonedSecrets(dest);
140
+ }
141
+
142
+ /**
143
+ * Delete donor secret rows from a freshly cloned state DB (keys only, values
144
+ * are never read). Returns the number of deleted rows.
145
+ * @param {string} stateDbPath
146
+ */
147
+ export function scrubClonedSecrets(stateDbPath) {
148
+ let scrubbed = 0;
149
+ for (const pattern of CLONE_SCRUB_LIKE_PATTERNS) {
150
+ const out = execFileSync(
151
+ "sqlite3",
152
+ [
153
+ stateDbPath,
154
+ `DELETE FROM ItemTable WHERE key LIKE '${pattern}'; SELECT changes();`,
155
+ ],
156
+ { encoding: "utf8" },
157
+ );
158
+ scrubbed += parseInt(out.trim(), 10) || 0;
159
+ }
160
+ return scrubbed;
129
161
  }
130
162
 
131
163
  function shellQuote(s) {
@@ -150,19 +182,23 @@ export function needsFirstRunChecklist(profileDir) {
150
182
  */
151
183
  export function buildFirstRunChecklist(
152
184
  projectName,
153
- baseUrl = "https://gateway.example.com/v1",
185
+ baseUrl = "",
154
186
  platform = process.platform,
155
187
  ) {
188
+ const gateway = baseUrl || "https://gateway.example.com/v1";
189
+ const gatewayStep = baseUrl
190
+ ? ""
191
+ : ` cursor-keys config baseUrl <your-gateway-url> # one-time, e.g. ${gateway}\n`;
156
192
  if (platform === "darwin") {
157
193
  return `
158
194
  First-time setup for "${projectName}" · copy-paste in order:
159
195
 
160
- cursor-keys clone ${projectName} # inherit sign-in, settings, model config from main Cursor
196
+ ${gatewayStep} cursor-keys clone ${projectName} # inherit sign-in, settings, model config from main Cursor
161
197
  cursor-keys set-key ${projectName} # inject the project API key (encrypted, hidden prompt)
162
198
  cursor-keys doctor ${projectName} # verify the profile
163
199
 
164
200
  Manual alternative: Cursor Settings → Models → API keys, paste the key under
165
- OpenAI API Key, enable "Override OpenAI Base URL" with: ${baseUrl}
201
+ OpenAI API Key, enable "Override OpenAI Base URL" with: ${gateway}
166
202
 
167
203
  Already inside this profile's window? Close it before running clone or set-key.
168
204
  `;
@@ -171,12 +207,12 @@ Already inside this profile's window? Close it before running clone or set-key.
171
207
  return `
172
208
  First-time setup for "${projectName}" · copy-paste in order:
173
209
 
174
- cursor-keys clone ${projectName} # inherit sign-in, settings, model config from main Cursor
210
+ ${gatewayStep} cursor-keys clone ${projectName} # inherit sign-in, settings, model config from main Cursor
175
211
  cursor-keys open ${projectName} # open once so Cursor creates its state
176
212
  cursor-keys doctor ${projectName} # verify the profile
177
213
 
178
214
  Then in that window: Cursor Settings → Models → API keys, paste the key under
179
- OpenAI API Key, enable "Override OpenAI Base URL" with: ${baseUrl}
215
+ OpenAI API Key, enable "Override OpenAI Base URL" with: ${gateway}
180
216
 
181
217
  Already inside this profile's window? Close it before running clone.
182
218
  `;
@@ -193,9 +229,11 @@ export function createProfile({ profileDir, projectName, color, cloneFrom }) {
193
229
  fs.mkdirSync(profileDir, { recursive: true });
194
230
  writeProfileSettings(profileDir, projectName, color);
195
231
  symlinkSharedUserFiles(profileDir);
232
+ let scrubbed = 0;
196
233
  if (cloneFrom) {
197
- cloneStateDb(cloneFrom, profileDir);
234
+ scrubbed = cloneStateDb(cloneFrom, profileDir);
198
235
  }
236
+ return { scrubbed };
199
237
  }
200
238
 
201
239
  export function profileIntegrityChecks(profileDir) {
@@ -1,58 +0,0 @@
1
- // Postinstall: one-time hint + best-effort companion extension install.
2
- // Never breaks the install: the whole body is guarded and exits 0.
3
- // Quiet installs: CURSOR_KEYS_NO_POSTINSTALL=1
4
- import fs from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { fileURLToPath } from "node:url";
8
-
9
- try {
10
- if (process.env.CURSOR_KEYS_NO_POSTINSTALL) process.exit(0);
11
- const yellow = "\x1b[33m";
12
- const dim = "\x1b[2m";
13
- const reset = "\x1b[0m";
14
-
15
- const pkgRoot = path.join(
16
- path.dirname(fileURLToPath(import.meta.url)),
17
- "..",
18
- );
19
- const extSrc = path.join(pkgRoot, "extension");
20
- const extPkgPath = path.join(extSrc, "package.json");
21
- const cursorDir = path.join(os.homedir(), ".cursor");
22
-
23
- // Auto-link the companion extension on global installs when Cursor exists.
24
- let extLine = "";
25
- const isGlobal = process.env.npm_config_global === "true";
26
- if (isGlobal && fs.existsSync(cursorDir) && fs.existsSync(extPkgPath)) {
27
- try {
28
- const extPkg = JSON.parse(fs.readFileSync(extPkgPath, "utf8"));
29
- const extDir = path.join(cursorDir, "extensions");
30
- fs.mkdirSync(extDir, { recursive: true });
31
- const target = path.join(
32
- extDir,
33
- `${extPkg.publisher || "lubu"}.${extPkg.name}-${extPkg.version}`,
34
- );
35
- fs.rmSync(target, { recursive: true, force: true });
36
- try {
37
- fs.symlinkSync(extSrc, target, "dir");
38
- } catch {
39
- fs.cpSync(extSrc, target, { recursive: true });
40
- }
41
- extLine = `\n Cursor extension linked ${dim}· restart Cursor, then Cmd+Shift+P: "cursor-keys: Open Project..."${reset}`;
42
- } catch {
43
- extLine = `\n cursor-keys install-extension link the Cursor extension (status bar + warnings)`;
44
- }
45
- }
46
-
47
- console.log(`
48
- ${yellow}cursor-keys installed${reset}
49
-
50
- cursor-keys --help commands
51
- cursor-keys aliases --install aliases, 'ck' shortcut, tab completion
52
- cursor-keys wrap --install plain 'cursor <path>' picks the profile
53
- ${extLine}
54
- ${dim}Quiet installs: CURSOR_KEYS_NO_POSTINSTALL=1${reset}
55
- `);
56
- } catch {
57
- /* postinstall must never break install */
58
- }