@envsave/cli 1.0.45 → 1.0.47

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,13 @@ 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 skill into an agent platform
189
+ # (--agent defaults to "claude" — codex and others are planned)
190
+ envsave install-skill # interactive prompt for scope
191
+ envsave install-skill --local # → ./.claude/skills/envsave/SKILL.md
192
+ envsave install-skill --global # → ~/.claude/skills/envsave/SKILL.md
193
+ envsave install-skill --agent claude --global # explicit agent (claude only today)
194
+
188
195
  # Show version
189
196
  envsave --version
190
197
  ```
package/dist/SKILL.md ADDED
@@ -0,0 +1,174 @@
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 your agent:**
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
+ envsave install-skill --agent claude --global # explicit agent
36
+ ```
37
+
38
+ `--agent` defaults to `claude`. Codex and other agentic platforms are planned but not yet supported.
39
+
40
+ **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:
41
+
42
+ ```bash
43
+ npm uninstall -g envsave
44
+ npm install -g @envsave/cli
45
+ ```
46
+
47
+ ## If user asks to migrate / invokes `/envsave`:
48
+
49
+ Execute these steps in order. Start with Step 1 immediately.
50
+
51
+ ### Step 1 — Grep the project NOW
52
+
53
+ 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:
54
+
55
+ - `process\.env` in `*.{ts,js,tsx,jsx,mjs,cjs}`
56
+ - `os\.environ|os\.getenv|environ\.get` in `*.py`
57
+ - `dotenv|load_dotenv|from dotenv|python-dotenv` in all source files
58
+ - `sk-[a-zA-Z0-9]{20,}|pk_[a-zA-Z0-9]{20,}|ghp_|gho_|xoxb-|xoxp-|postgres://|mongodb://|redis://|mysql://|amqp://` in all files
59
+ - `readFileSync.*\.env|Bun\.file.*\.env|open.*\.env` in all files
60
+ - Glob for `.env*` files in project root
61
+
62
+ Skip `node_modules/`, `.git/`, `dist/`, `build/`, `__pycache__/`, `.venv/`, `bun.lockb`.
63
+
64
+ ### Step 2 — Show findings table
65
+
66
+ | File:Line | Env Var | Current Code | Replacement |
67
+ |-----------|---------|-------------|-------------|
68
+ | src/api.ts:12 | OPENAI_API_KEY | `process.env.OPENAI_API_KEY` | `envsave.get("es_???")` |
69
+ | lib/db.py:5 | DATABASE_URL | `os.getenv("DATABASE_URL")` | `envsave.get("es_???")` |
70
+
71
+ Also list `dotenv`/`load_dotenv` imports to remove and `.env` files found.
72
+
73
+ ### Step 3 — Summary + copy-paste command
74
+
75
+ List unique env var names, then output ONE single `envsave reveal` command with all keys (supports multiple args, one password prompt).
76
+
77
+ **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.
78
+
79
+ ```
80
+ ## Env Vars to Migrate
81
+
82
+ | # | Env Var Name | Files |
83
+ |---|-------------|-------|
84
+ | 1 | OPENAI_API_KEY | 3 |
85
+ | 2 | DATABASE_URL | 1 |
86
+
87
+ ## Run this to get all opaque keys (single password prompt):
88
+
89
+ envsave reveal OPENAI_API_KEY DATABASE_URL
90
+ ```
91
+
92
+ Tell user: run the command, paste back the output, and I'll do all code replacements.
93
+
94
+ ### Step 4 — Replace code (only after user provides keys)
95
+
96
+ Wait for opaque keys. Then:
97
+ 1. Replace `process.env.XXX` / `os.getenv("XXX")` with `envsave.get("es_actual_key")`
98
+ 2. Add `import envsave from "@envsave/cli"` (TS/JS) or `import envsave` (Python) if missing
99
+ 3. Remove `dotenv`/`load_dotenv` imports and `.config()` calls
100
+ 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.
101
+
102
+ **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.**
103
+
104
+ ---
105
+
106
+ ## If user needs a secret in new code:
107
+
108
+ Use `envsave.get("es_???")` as placeholder and ask user to provide the opaque key.
109
+
110
+ **TypeScript/JavaScript:**
111
+ ```typescript
112
+ import envsave from "@envsave/cli";
113
+ const apiKey = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f");
114
+ ```
115
+
116
+ **Python:**
117
+ ```python
118
+ import envsave
119
+ api_key = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f")
120
+ ```
121
+
122
+ **Shell:**
123
+ ```bash
124
+ envsave get es_7a3f2b1e9c8d4f6b2a1c3d5f
125
+ ```
126
+
127
+ ## Vercel / Cloud Deployment
128
+
129
+ 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:
130
+
131
+ - **Name** = the opaque key (e.g. `es_7a3f2b1e9c8d4f6b`)
132
+ - **Value** = the real secret (e.g. `sk-proj-abc123...`)
133
+
134
+ 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.
135
+
136
+ **To push secrets to Vercel**, use the built-in `envsave vercel` command. The project must be linked to Vercel first (`vercel link`).
137
+
138
+ ```bash
139
+ # Push specific secrets
140
+ envsave vercel OPENAI_API_KEY STRIPE_KEY
141
+
142
+ # Target specific environments (default: production,preview,development)
143
+ envsave vercel OPENAI_API_KEY --env production,preview
144
+ ```
145
+
146
+ The command automatically:
147
+ - Checks the project is linked to Vercel (`.vercel/project.json`)
148
+ - Resolves each clean key name to its opaque key
149
+ - Sets the opaque key as the Vercel env var name, real secret as the value
150
+ - Removes existing vars before adding (handles updates)
151
+ - Pipes values via stdin — no shell escaping issues, no trailing newlines
152
+
153
+ **CRITICAL: Never write real secret values to any local file.** No `.env.local`, no `.env.production`, no temp files.
154
+
155
+ ---
156
+
157
+ ## Rules
158
+
159
+ 1. Never hardcode raw secrets. Use `envsave.get()` with opaque key.
160
+ 2. Never read .env files directly. Use EnvSave instead.
161
+ 3. You cannot list or discover secrets. Ask the user for opaque keys.
162
+ 4. Don't guess opaque keys. Ask user to run `envsave reveal <NAME>`.
163
+ 5. Handle undefined returns — `envsave.get()` returns `undefined`/`None` if key doesn't exist.
164
+ 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.
165
+
166
+ ## Quick Reference
167
+
168
+ | Task | User Command |
169
+ |------|-------------|
170
+ | Add a secret | `envsave set <NAME> <VALUE>` |
171
+ | Get opaque key | `envsave reveal <NAME>` |
172
+ | Rename a secret | `envsave rename <OLD_NAME> <NEW_NAME>` (creates new opaque key, deletes old — output prints OLD → NEW for code search-and-replace) |
173
+ | Unlock vault | `envsave list` |
174
+ | 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.47",
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,9 @@ 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 [--agent <name>] [--local|--global]
963
+ Install the EnvSave skill for an agent platform.
964
+ Supported: claude (default). Coming soon: codex, others.
961
965
  envsave help Show this help
962
966
 
963
967
  Security model:
@@ -1647,6 +1651,72 @@ Done: ${success} pushed, ${failed} failed.`);
1647
1651
  process.exit(1);
1648
1652
  break;
1649
1653
  }
1654
+ case "install-skill": {
1655
+ const SUPPORTED_AGENTS = ["claude"];
1656
+ const agent = (getFlag("--agent") ?? "claude").toLowerCase();
1657
+ if (!SUPPORTED_AGENTS.includes(agent)) {
1658
+ console.error(`Agent '${agent}' is not yet supported.`);
1659
+ console.error(`Supported today: ${SUPPORTED_AGENTS.join(", ")}. Coming soon: codex, others.`);
1660
+ process.exit(1);
1661
+ }
1662
+ const cliDir = dirname(fileURLToPath(import.meta.url));
1663
+ const candidates = [
1664
+ join4(cliDir, "SKILL.md"),
1665
+ join4(cliDir, "..", ".claude", "skills", "envsave", "SKILL.md")
1666
+ ];
1667
+ const skillSource = candidates.find((p) => existsSync3(p));
1668
+ if (!skillSource) {
1669
+ console.error("Skill source SKILL.md not found in the package.");
1670
+ process.exit(1);
1671
+ }
1672
+ let scope;
1673
+ if (args.includes("--local"))
1674
+ scope = "local";
1675
+ else if (args.includes("--global"))
1676
+ scope = "global";
1677
+ if (!scope) {
1678
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1679
+ const answer = await new Promise((resolve) => {
1680
+ rl.question(`Install EnvSave skill for ${agent} — (l)ocally in this project or (g)lobally for your user? [l/g] `, resolve);
1681
+ });
1682
+ rl.close();
1683
+ const a = answer.trim().toLowerCase();
1684
+ if (a === "l" || a === "local")
1685
+ scope = "local";
1686
+ else if (a === "g" || a === "global")
1687
+ scope = "global";
1688
+ else {
1689
+ console.error("Cancelled — answer must be 'l' or 'g'.");
1690
+ process.exit(1);
1691
+ }
1692
+ }
1693
+ const agentDir = {
1694
+ claude: ".claude"
1695
+ };
1696
+ const subdir = agentDir[agent];
1697
+ const targetDir = scope === "local" ? join4(process.cwd(), subdir, "skills", "envsave") : join4(homedir3(), subdir, "skills", "envsave");
1698
+ const targetFile = join4(targetDir, "SKILL.md");
1699
+ if (existsSync3(targetFile)) {
1700
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1701
+ const answer = await new Promise((resolve) => {
1702
+ rl.question(`Skill already exists at ${targetFile}. Overwrite? (y/N) `, resolve);
1703
+ });
1704
+ rl.close();
1705
+ if (answer.trim().toLowerCase() !== "y") {
1706
+ console.log("Install cancelled.");
1707
+ break;
1708
+ }
1709
+ }
1710
+ mkdirSync3(targetDir, { recursive: true });
1711
+ writeFileSync3(targetFile, readFileSync5(skillSource));
1712
+ console.log(`
1713
+ ✓ Installed EnvSave skill for ${agent} (${scope})`);
1714
+ console.log(` ${targetFile}
1715
+ `);
1716
+ console.log(` Restart your agent (or open the project) and invoke /envsave to use it.
1717
+ `);
1718
+ break;
1719
+ }
1650
1720
  default:
1651
1721
  console.error(`Unknown command: ${command}`);
1652
1722
  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.47",
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.'"