@envsave/cli 1.0.44 → 1.0.46

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
@@ -161,6 +161,17 @@ envsave list
161
161
  # Delete a secret (password required)
162
162
  envsave delete OPENAI_API_KEY
163
163
 
164
+ # Rename a secret (password required)
165
+ # Creates a new opaque key derived from the new name, copies the value over,
166
+ # and deletes the old opaque key. The output prints OLD → NEW for easy
167
+ # search-and-replace in your code.
168
+ envsave rename CLERK_SECRET_KEY_THEHOTELAGENTS CLERK_SECRET_KEY_DIRECTBOOKAI
169
+ # Output:
170
+ # Renamed: CLERK_SECRET_KEY_THEHOTELAGENTS → CLERK_SECRET_KEY_DIRECTBOOKAI
171
+ # Replace in your code:
172
+ # OLD (deleted): es_7cXxcbsVaJQv...
173
+ # NEW (created): es_enLaxEjzTO5r...
174
+
164
175
  # Bulk import from .env file (password required)
165
176
  envsave import .env
166
177
 
@@ -174,6 +185,11 @@ envsave passwd
174
185
  envsave config # show current
175
186
  envsave config --max-gets 120 # set max gets/minute
176
187
 
188
+ # Install the EnvSave Claude skill (prompts: local in project / global for user)
189
+ envsave install-skill # interactive
190
+ envsave install-skill --local # → ./.claude/skills/envsave/SKILL.md
191
+ envsave install-skill --global # → ~/.claude/skills/envsave/SKILL.md
192
+
177
193
  # Show version
178
194
  envsave --version
179
195
  ```
package/dist/SKILL.md ADDED
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: envsave
3
+ description: Use EnvSave to securely retrieve secrets via opaque keys instead of hardcoding them. Trigger whenever code needs API keys, tokens, passwords, database URLs, connection strings, webhook secrets, credentials, environment variables, or any sensitive value. Also trigger when you see hardcoded secrets in existing code that should be migrated to EnvSave, when .env files are being read directly, or when the user mentions secrets, credentials, or vault.
4
+ user_invocable: true
5
+ ---
6
+
7
+ # EnvSave Migration & Usage Skill
8
+
9
+ When this skill is triggered, follow the instructions below. Everything you need to know is in this file. Do not research, explore, or investigate envsave — just execute the steps.
10
+
11
+ ## Installation
12
+
13
+ EnvSave is installed from **npm** (not from a local path on the user's machine). The npm package is scoped: `@envsave/cli`.
14
+
15
+ ```bash
16
+ # Global CLI (the `envsave` binary)
17
+ npm install -g @envsave/cli
18
+
19
+ # As a library in a project
20
+ npm install @envsave/cli
21
+ # or: bun add @envsave/cli
22
+
23
+ # Python
24
+ pip install envsave
25
+ ```
26
+
27
+ If the user's project doesn't yet depend on envsave, add `@envsave/cli` to their dependencies before emitting `import` statements. Never tell the user to link from a local checkout — always use the npm package.
28
+
29
+ **Installing this skill in Claude Code:**
30
+
31
+ ```bash
32
+ envsave install-skill # interactive — asks local or global
33
+ envsave install-skill --local # → ./.claude/skills/envsave/SKILL.md (this project only)
34
+ envsave install-skill --global # → ~/.claude/skills/envsave/SKILL.md (all your projects)
35
+ ```
36
+
37
+ **Upgrading from the old unscoped package:** if `npm install -g @envsave/cli` fails with `EEXIST` on `bin/envsave`, the user has the old unscoped `envsave` package installed globally (pre-rename). Fix it with:
38
+
39
+ ```bash
40
+ npm uninstall -g envsave
41
+ npm install -g @envsave/cli
42
+ ```
43
+
44
+ ## If user asks to migrate / invokes `/envsave`:
45
+
46
+ Execute these steps in order. Start with Step 1 immediately.
47
+
48
+ ### Step 1 — Grep the project NOW
49
+
50
+ Use the Grep tool (not Bash, not Agent, not Explore) to search the current project directory. Run ALL of these searches in parallel in a single response:
51
+
52
+ - `process\.env` in `*.{ts,js,tsx,jsx,mjs,cjs}`
53
+ - `os\.environ|os\.getenv|environ\.get` in `*.py`
54
+ - `dotenv|load_dotenv|from dotenv|python-dotenv` in all source files
55
+ - `sk-[a-zA-Z0-9]{20,}|pk_[a-zA-Z0-9]{20,}|ghp_|gho_|xoxb-|xoxp-|postgres://|mongodb://|redis://|mysql://|amqp://` in all files
56
+ - `readFileSync.*\.env|Bun\.file.*\.env|open.*\.env` in all files
57
+ - Glob for `.env*` files in project root
58
+
59
+ Skip `node_modules/`, `.git/`, `dist/`, `build/`, `__pycache__/`, `.venv/`, `bun.lockb`.
60
+
61
+ ### Step 2 — Show findings table
62
+
63
+ | File:Line | Env Var | Current Code | Replacement |
64
+ |-----------|---------|-------------|-------------|
65
+ | src/api.ts:12 | OPENAI_API_KEY | `process.env.OPENAI_API_KEY` | `envsave.get("es_???")` |
66
+ | lib/db.py:5 | DATABASE_URL | `os.getenv("DATABASE_URL")` | `envsave.get("es_???")` |
67
+
68
+ Also list `dotenv`/`load_dotenv` imports to remove and `.env` files found.
69
+
70
+ ### Step 3 — Summary + copy-paste command
71
+
72
+ List unique env var names, then output ONE single `envsave reveal` command with all keys (supports multiple args, one password prompt).
73
+
74
+ **IMPORTANT: The command MUST be on a single line with no line breaks.** If the list of keys is long, still keep it on one line. Never wrap or split the command across multiple lines — line breaks will break copy-paste in the terminal.
75
+
76
+ ```
77
+ ## Env Vars to Migrate
78
+
79
+ | # | Env Var Name | Files |
80
+ |---|-------------|-------|
81
+ | 1 | OPENAI_API_KEY | 3 |
82
+ | 2 | DATABASE_URL | 1 |
83
+
84
+ ## Run this to get all opaque keys (single password prompt):
85
+
86
+ envsave reveal OPENAI_API_KEY DATABASE_URL
87
+ ```
88
+
89
+ Tell user: run the command, paste back the output, and I'll do all code replacements.
90
+
91
+ ### Step 4 — Replace code (only after user provides keys)
92
+
93
+ Wait for opaque keys. Then:
94
+ 1. Replace `process.env.XXX` / `os.getenv("XXX")` with `envsave.get("es_actual_key")`
95
+ 2. Add `import envsave from "@envsave/cli"` (TS/JS) or `import envsave` (Python) if missing
96
+ 3. Remove `dotenv`/`load_dotenv` imports and `.config()` calls
97
+ 4. List all `.env*` files found (`.env`, `.env.local`, `.env.production`, etc.) and **tell the user they must delete them** — these files contain real secrets in plain text and are no longer needed after migration to envsave. Add them to `.gitignore` if not already there.
98
+
99
+ **IMPORTANT: Always use `envsave.get("es_...")` directly — the full module call. Never create wrapper functions like `envsave_get()`, `get_secret()`, `_get_env()`, or similar. The call should always be `envsave.get("es_...")` exactly.**
100
+
101
+ ---
102
+
103
+ ## If user needs a secret in new code:
104
+
105
+ Use `envsave.get("es_???")` as placeholder and ask user to provide the opaque key.
106
+
107
+ **TypeScript/JavaScript:**
108
+ ```typescript
109
+ import envsave from "@envsave/cli";
110
+ const apiKey = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f");
111
+ ```
112
+
113
+ **Python:**
114
+ ```python
115
+ import envsave
116
+ api_key = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f")
117
+ ```
118
+
119
+ **Shell:**
120
+ ```bash
121
+ envsave get es_7a3f2b1e9c8d4f6b2a1c3d5f
122
+ ```
123
+
124
+ ## Vercel / Cloud Deployment
125
+
126
+ On Vercel there is no envsave vault. When `envsave.get("es_xxx")` can't find the vault, it falls back to `process.env["es_xxx"]`. So you need to set Vercel env vars where:
127
+
128
+ - **Name** = the opaque key (e.g. `es_7a3f2b1e9c8d4f6b`)
129
+ - **Value** = the real secret (e.g. `sk-proj-abc123...`)
130
+
131
+ This way the code `envsave.get("es_7a3f2b1e9c8d4f6b")` resolves via `process.env.es_7a3f2b1e9c8d4f6b` on Vercel, returning the real API key — without any vault needed.
132
+
133
+ **To push secrets to Vercel**, use the built-in `envsave vercel` command. The project must be linked to Vercel first (`vercel link`).
134
+
135
+ ```bash
136
+ # Push specific secrets
137
+ envsave vercel OPENAI_API_KEY STRIPE_KEY
138
+
139
+ # Target specific environments (default: production,preview,development)
140
+ envsave vercel OPENAI_API_KEY --env production,preview
141
+ ```
142
+
143
+ The command automatically:
144
+ - Checks the project is linked to Vercel (`.vercel/project.json`)
145
+ - Resolves each clean key name to its opaque key
146
+ - Sets the opaque key as the Vercel env var name, real secret as the value
147
+ - Removes existing vars before adding (handles updates)
148
+ - Pipes values via stdin — no shell escaping issues, no trailing newlines
149
+
150
+ **CRITICAL: Never write real secret values to any local file.** No `.env.local`, no `.env.production`, no temp files.
151
+
152
+ ---
153
+
154
+ ## Rules
155
+
156
+ 1. Never hardcode raw secrets. Use `envsave.get()` with opaque key.
157
+ 2. Never read .env files directly. Use EnvSave instead.
158
+ 3. You cannot list or discover secrets. Ask the user for opaque keys.
159
+ 4. Don't guess opaque keys. Ask user to run `envsave reveal <NAME>`.
160
+ 5. Handle undefined returns — `envsave.get()` returns `undefined`/`None` if key doesn't exist.
161
+ 6. **Never write real secret values to any local file** (no `.env.local`, `.env.production`, etc.). For cloud deployments (Vercel, etc.), pipe from `envsave get` directly to the platform CLI.
162
+
163
+ ## Quick Reference
164
+
165
+ | Task | User Command |
166
+ |------|-------------|
167
+ | Add a secret | `envsave set <NAME> <VALUE>` |
168
+ | Get opaque key | `envsave reveal <NAME>` |
169
+ | Rename a secret | `envsave rename <OLD_NAME> <NEW_NAME>` (creates new opaque key, deletes old — output prints OLD → NEW for code search-and-replace) |
170
+ | Unlock vault | `envsave list` |
171
+ | Push to Vercel | `envsave vercel KEY1 KEY2` |
package/dist/cli.js CHANGED
@@ -100,6 +100,7 @@ __export(exports_vault, {
100
100
  searchKeys: () => searchKeys,
101
101
  revealKeysByPattern: () => revealKeysByPattern,
102
102
  revealKey: () => revealKey,
103
+ renameSecret: () => renameSecret,
103
104
  listKeys: () => listKeys,
104
105
  initVault: () => initVault,
105
106
  importEnv: () => importEnv,
@@ -446,6 +447,28 @@ function deleteSecret(key, password) {
446
447
  writeSessionCache(vault.secrets, vault.registry);
447
448
  return true;
448
449
  }
450
+ function renameSecret(oldName, newName, password) {
451
+ if (oldName === newName) {
452
+ throw new Error("Old and new names are the same.");
453
+ }
454
+ const vault = readMainVault(password);
455
+ const sshKeyContent = loadSshKey(vault);
456
+ const oldOpaque = computeOpaqueName(vault.hmacSeed, oldName, sshKeyContent);
457
+ if (!vault.registry.includes(oldName) || !(oldOpaque in vault.secrets)) {
458
+ throw new Error(`Secret '${oldName}' not found.`);
459
+ }
460
+ if (vault.registry.includes(newName)) {
461
+ throw new Error(`Secret '${newName}' already exists.`);
462
+ }
463
+ const newOpaque = computeOpaqueName(vault.hmacSeed, newName, sshKeyContent);
464
+ const value = vault.secrets[oldOpaque];
465
+ vault.secrets[newOpaque] = value;
466
+ delete vault.secrets[oldOpaque];
467
+ vault.registry = vault.registry.map((k) => k === oldName ? newName : k);
468
+ writeMainVault(vault, password);
469
+ writeSessionCache(vault.secrets, vault.registry);
470
+ return { oldOpaque, newOpaque };
471
+ }
449
472
  function listKeys(password) {
450
473
  const vault = readMainVault(password);
451
474
  writeSessionCache(vault.secrets, vault.registry);
@@ -688,7 +711,8 @@ import { createInterface } from "node:readline";
688
711
  import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
689
712
  import { spawnSync } from "node:child_process";
690
713
  import { createHash } from "node:crypto";
691
- import { join as join4 } from "node:path";
714
+ import { dirname, join as join4 } from "node:path";
715
+ import { fileURLToPath } from "node:url";
692
716
  import { homedir as homedir3 } from "node:os";
693
717
 
694
718
  // src/license.ts
@@ -836,7 +860,7 @@ async function checkLicense() {
836
860
  // package.json
837
861
  var package_default = {
838
862
  name: "@envsave/cli",
839
- version: "1.0.44",
863
+ version: "1.0.46",
840
864
  description: "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
841
865
  type: "module",
842
866
  main: "dist/index.js",
@@ -850,7 +874,7 @@ var package_default = {
850
874
  ],
851
875
  scripts: {
852
876
  prebuild: `bun -e "const p=require('./package.json');const v=p.version.split('.');v[2]=+v[2]+1;p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')"`,
853
- build: "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js",
877
+ build: "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js && cp .claude/skills/envsave/SKILL.md dist/SKILL.md",
854
878
  dev: "bun run src/cli.ts",
855
879
  webapp: "bun run webapp/server.ts",
856
880
  "lint:claude": "claude -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"
@@ -922,6 +946,7 @@ Usage:
922
946
  envsave show <key> Show real value by key name (password required)
923
947
  envsave reveal <keyword> [keyword2]... Show opaque key(s) for any name matching keyword (password required)
924
948
  envsave delete <key> Remove a secret (password required)
949
+ envsave rename <old> <new> Rename a secret (creates new opaque key, deletes old) (password required)
925
950
  envsave search <keyword> Search keys matching keyword (no password)
926
951
  envsave scan [dir] Scan project for env variable usage
927
952
  envsave passwd Change master password
@@ -934,6 +959,7 @@ Usage:
934
959
  envsave restore Download vault backup from server
935
960
  envsave sync Sync vault with server (pull if cloud newer)
936
961
  envsave vercel <KEY|es_key>... Push secrets to Vercel under both clean name + opaque id
962
+ envsave install-skill [--local|--global] Install the EnvSave Claude skill (prompts if no flag)
937
963
  envsave help Show this help
938
964
 
939
965
  Security model:
@@ -1168,6 +1194,28 @@ Use 'envsave reveal <KEY>' to get the opaque key for each secret.`);
1168
1194
  }
1169
1195
  break;
1170
1196
  }
1197
+ case "rename": {
1198
+ const oldName = args[1];
1199
+ const newName = args[2];
1200
+ if (!oldName || !newName) {
1201
+ console.error("Usage: envsave rename <old> <new>");
1202
+ process.exit(1);
1203
+ }
1204
+ const password = await requirePassword();
1205
+ const { oldOpaque, newOpaque } = renameSecret(oldName, newName, password);
1206
+ console.log(`
1207
+ Renamed: ${oldName} → ${newName}`);
1208
+ console.log(`
1209
+ Replace in your code:`);
1210
+ console.log(` OLD (deleted): ${oldOpaque}`);
1211
+ console.log(` NEW (created): ${newOpaque}`);
1212
+ console.log(`
1213
+ Or replace the full call:`);
1214
+ console.log(` envsave.get("${oldOpaque}")`);
1215
+ console.log(` envsave.get("${newOpaque}")
1216
+ `);
1217
+ break;
1218
+ }
1171
1219
  case "config": {
1172
1220
  const maxGets = getFlag("--max-gets");
1173
1221
  if (!maxGets) {
@@ -1601,6 +1649,61 @@ Done: ${success} pushed, ${failed} failed.`);
1601
1649
  process.exit(1);
1602
1650
  break;
1603
1651
  }
1652
+ case "install-skill": {
1653
+ const cliDir = dirname(fileURLToPath(import.meta.url));
1654
+ const candidates = [
1655
+ join4(cliDir, "SKILL.md"),
1656
+ join4(cliDir, "..", ".claude", "skills", "envsave", "SKILL.md")
1657
+ ];
1658
+ const skillSource = candidates.find((p) => existsSync3(p));
1659
+ if (!skillSource) {
1660
+ console.error("Skill source SKILL.md not found in the package.");
1661
+ process.exit(1);
1662
+ }
1663
+ let scope;
1664
+ if (args.includes("--local"))
1665
+ scope = "local";
1666
+ else if (args.includes("--global"))
1667
+ scope = "global";
1668
+ if (!scope) {
1669
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1670
+ const answer = await new Promise((resolve) => {
1671
+ rl.question("Install EnvSave skill (l)ocally in this project or (g)lobally for your user? [l/g] ", resolve);
1672
+ });
1673
+ rl.close();
1674
+ const a = answer.trim().toLowerCase();
1675
+ if (a === "l" || a === "local")
1676
+ scope = "local";
1677
+ else if (a === "g" || a === "global")
1678
+ scope = "global";
1679
+ else {
1680
+ console.error("Cancelled — answer must be 'l' or 'g'.");
1681
+ process.exit(1);
1682
+ }
1683
+ }
1684
+ const targetDir = scope === "local" ? join4(process.cwd(), ".claude", "skills", "envsave") : join4(homedir3(), ".claude", "skills", "envsave");
1685
+ const targetFile = join4(targetDir, "SKILL.md");
1686
+ if (existsSync3(targetFile)) {
1687
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1688
+ const answer = await new Promise((resolve) => {
1689
+ rl.question(`Skill already exists at ${targetFile}. Overwrite? (y/N) `, resolve);
1690
+ });
1691
+ rl.close();
1692
+ if (answer.trim().toLowerCase() !== "y") {
1693
+ console.log("Install cancelled.");
1694
+ break;
1695
+ }
1696
+ }
1697
+ mkdirSync3(targetDir, { recursive: true });
1698
+ writeFileSync3(targetFile, readFileSync5(skillSource));
1699
+ console.log(`
1700
+ ✓ Installed EnvSave skill ${scope === "local" ? "locally" : "globally"}`);
1701
+ console.log(` ${targetFile}
1702
+ `);
1703
+ console.log(` Restart Claude Code (or open the project) and invoke /envsave to use it.
1704
+ `);
1705
+ break;
1706
+ }
1604
1707
  default:
1605
1708
  console.error(`Unknown command: ${command}`);
1606
1709
  usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsave/cli",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "prebuild": "bun -e \"const p=require('./package.json');const v=p.version.split('.');v[2]=+v[2]+1;p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"",
17
- "build": "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js",
17
+ "build": "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js && cp .claude/skills/envsave/SKILL.md dist/SKILL.md",
18
18
  "dev": "bun run src/cli.ts",
19
19
  "webapp": "bun run webapp/server.ts",
20
20
  "lint:claude": "claude -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"