@envsave/cli 1.0.45 → 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
@@ -185,6 +185,11 @@ envsave passwd
185
185
  envsave config # show current
186
186
  envsave config --max-gets 120 # set max gets/minute
187
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
+
188
193
  # Show version
189
194
  envsave --version
190
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
@@ -711,7 +711,8 @@ import { createInterface } from "node:readline";
711
711
  import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
712
712
  import { spawnSync } from "node:child_process";
713
713
  import { createHash } from "node:crypto";
714
- import { join as join4 } from "node:path";
714
+ import { dirname, join as join4 } from "node:path";
715
+ import { fileURLToPath } from "node:url";
715
716
  import { homedir as homedir3 } from "node:os";
716
717
 
717
718
  // src/license.ts
@@ -859,7 +860,7 @@ async function checkLicense() {
859
860
  // package.json
860
861
  var package_default = {
861
862
  name: "@envsave/cli",
862
- version: "1.0.45",
863
+ version: "1.0.46",
863
864
  description: "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
864
865
  type: "module",
865
866
  main: "dist/index.js",
@@ -873,7 +874,7 @@ var package_default = {
873
874
  ],
874
875
  scripts: {
875
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')"`,
876
- 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",
877
878
  dev: "bun run src/cli.ts",
878
879
  webapp: "bun run webapp/server.ts",
879
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.'"
@@ -958,6 +959,7 @@ Usage:
958
959
  envsave restore Download vault backup from server
959
960
  envsave sync Sync vault with server (pull if cloud newer)
960
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)
961
963
  envsave help Show this help
962
964
 
963
965
  Security model:
@@ -1647,6 +1649,61 @@ Done: ${success} pushed, ${failed} failed.`);
1647
1649
  process.exit(1);
1648
1650
  break;
1649
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
+ }
1650
1707
  default:
1651
1708
  console.error(`Unknown command: ${command}`);
1652
1709
  usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsave/cli",
3
- "version": "1.0.45",
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.'"