cc-wrapper 0.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.
- package/README.md +294 -0
- package/dist/cli.js +329 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# cc-wrapper
|
|
2
|
+
|
|
3
|
+
> Cross-platform CLI wrapper for [Claude Code](https://claude.ai/code) with named environment profiles, arg passthrough, and zero config friction.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/cc-wrapper)
|
|
6
|
+
[](https://www.npmjs.com/package/cc-wrapper)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](#)
|
|
10
|
+
|
|
11
|
+
Switch between Claude environments — local proxies, OpenRouter, AWS Bedrock, Google Vertex AI — with a single command instead of exporting env vars every time.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Before cc-wrapper
|
|
15
|
+
ANTHROPIC_BASE_URL=http://localhost:8787 ANTHROPIC_AUTH_TOKEN=my-token claude
|
|
16
|
+
|
|
17
|
+
# After cc-wrapper
|
|
18
|
+
cc-wrapper claude
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Table of Contents
|
|
24
|
+
|
|
25
|
+
- [Features](#features)
|
|
26
|
+
- [Requirements](#requirements)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Quick Start](#quick-start)
|
|
29
|
+
- [Commands](#commands)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
- [Supported Environment Variables](#supported-environment-variables)
|
|
32
|
+
- [Security](#security)
|
|
33
|
+
- [Contributing](#contributing)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Named profiles** — save multiple Claude environments (local, openrouter, vertex, bedrock, etc.)
|
|
41
|
+
- **Interactive setup** — searchable env var selector with descriptions and example values
|
|
42
|
+
- **Auto `--dangerously-skip-permissions`** — injected by default, disable with `--dd`
|
|
43
|
+
- **Full arg passthrough** — all extra args forwarded to `claude` unchanged
|
|
44
|
+
- **Cross-platform** — Windows, macOS, Linux; config stored at correct OS path automatically
|
|
45
|
+
- **Secret masking** — API keys and tokens masked in all log output
|
|
46
|
+
- **Live sync** — fetch latest Claude env vars from official docs with `npm run sync-envs`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- [Node.js](https://nodejs.org) >= 20
|
|
53
|
+
- [Claude Code CLI](https://claude.ai/code) installed and accessible as `claude` in PATH
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g cc-wrapper
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Verify:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cc-wrapper --version
|
|
67
|
+
cc-wrapper --help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
**1. Create your first profile:**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cc-wrapper new
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You will be prompted for:
|
|
81
|
+
- Profile name (e.g. `local`, `openrouter`, `vertex`)
|
|
82
|
+
- Which Claude env vars to set (searchable, numbered list with descriptions)
|
|
83
|
+
- Value for each selected env var (with examples shown inline)
|
|
84
|
+
- Default extra args to pass to `claude`
|
|
85
|
+
|
|
86
|
+
**2. Run Claude with the profile:**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cc-wrapper claude
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Commands
|
|
95
|
+
|
|
96
|
+
### `cc-wrapper new`
|
|
97
|
+
|
|
98
|
+
Create a new named profile interactively.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cc-wrapper new
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Flow:
|
|
105
|
+
1. Enter profile name
|
|
106
|
+
2. Multi-select env vars from numbered list (SPACE to toggle, ENTER to confirm)
|
|
107
|
+
3. Enter value for each selected var (example shown inline)
|
|
108
|
+
4. Enter default claude args
|
|
109
|
+
5. Profile saved — set as default if it's the first one
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `cc-wrapper list`
|
|
114
|
+
|
|
115
|
+
List all saved profiles. Default profile is highlighted.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cc-wrapper list
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
● local (default)
|
|
123
|
+
○ openrouter
|
|
124
|
+
○ vertex
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `cc-wrapper default <name>`
|
|
130
|
+
|
|
131
|
+
Set the default profile used by `cc-wrapper claude`.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cc-wrapper default openrouter
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `cc-wrapper edit <name>`
|
|
140
|
+
|
|
141
|
+
Interactively edit an existing profile. Current values are pre-filled and masked.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cc-wrapper edit local
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### `cc-wrapper delete <name>`
|
|
150
|
+
|
|
151
|
+
Delete a profile (confirmation prompt required).
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cc-wrapper delete vertex
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the deleted profile was the default, the next available profile becomes the new default automatically.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `cc-wrapper claude [args...]`
|
|
162
|
+
|
|
163
|
+
Launch `claude` with the default profile's environment variables injected.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cc-wrapper claude
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Pass extra args directly to claude:**
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cc-wrapper claude --print "explain this codebase"
|
|
173
|
+
cc-wrapper claude --model claude-opus-4-7-20250514
|
|
174
|
+
cc-wrapper claude --no-stream
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Disable automatic `--dangerously-skip-permissions` injection:**
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cc-wrapper claude --dd
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Use a specific profile instead of default:**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cc-wrapper claude --profile openrouter
|
|
187
|
+
cc-wrapper claude -p vertex --print "hello"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
Config is stored as a single JSON file at the OS-appropriate path:
|
|
195
|
+
|
|
196
|
+
| OS | Path |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Linux | `~/.config/cc-wrapper/config.json` |
|
|
199
|
+
| macOS | `~/Library/Preferences/cc-wrapper/config.json` |
|
|
200
|
+
| Windows | `%APPDATA%\cc-wrapper\config.json` |
|
|
201
|
+
|
|
202
|
+
### Config schema
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"default": "local",
|
|
207
|
+
"configs": {
|
|
208
|
+
"local": {
|
|
209
|
+
"env": {
|
|
210
|
+
"ANTHROPIC_BASE_URL": "http://localhost:8787",
|
|
211
|
+
"ANTHROPIC_AUTH_TOKEN": "my-token"
|
|
212
|
+
},
|
|
213
|
+
"args": []
|
|
214
|
+
},
|
|
215
|
+
"openrouter": {
|
|
216
|
+
"env": {
|
|
217
|
+
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api/v1",
|
|
218
|
+
"ANTHROPIC_API_KEY": "sk-or-..."
|
|
219
|
+
},
|
|
220
|
+
"args": ["--model", "anthropic/claude-opus-4"]
|
|
221
|
+
},
|
|
222
|
+
"vertex": {
|
|
223
|
+
"env": {
|
|
224
|
+
"CLAUDE_CODE_USE_VERTEX": "1",
|
|
225
|
+
"ANTHROPIC_VERTEX_PROJECT_ID": "my-gcp-project",
|
|
226
|
+
"CLOUD_ML_REGION": "us-central1"
|
|
227
|
+
},
|
|
228
|
+
"args": []
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
You can edit this file directly or use `cc-wrapper edit <name>`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Supported Environment Variables
|
|
239
|
+
|
|
240
|
+
cc-wrapper supports all official Claude Code environment variables. The list is stored in `src/data/env-vars.json` and can be refreshed from the official docs:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
npm run sync-envs
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Key variables include:
|
|
247
|
+
|
|
248
|
+
| Variable | Description |
|
|
249
|
+
|---|---|
|
|
250
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key |
|
|
251
|
+
| `ANTHROPIC_BASE_URL` | Custom API base URL (local proxy, OpenRouter, etc.) |
|
|
252
|
+
| `ANTHROPIC_AUTH_TOKEN` | Bearer token for custom auth |
|
|
253
|
+
| `ANTHROPIC_MODEL` | Override default model |
|
|
254
|
+
| `CLAUDE_CODE_USE_VERTEX` | Use Google Vertex AI |
|
|
255
|
+
| `CLAUDE_CODE_USE_BEDROCK` | Use AWS Bedrock |
|
|
256
|
+
| `AWS_REGION` | AWS region for Bedrock |
|
|
257
|
+
| `CLOUD_ML_REGION` | GCP region for Vertex |
|
|
258
|
+
| `HTTP_PROXY` / `HTTPS_PROXY` | Proxy settings |
|
|
259
|
+
| `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Override max output tokens |
|
|
260
|
+
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | Disable telemetry |
|
|
261
|
+
|
|
262
|
+
Full list: run `cc-wrapper new` and browse the interactive selector.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Security
|
|
267
|
+
|
|
268
|
+
- API keys and tokens are **never printed** in logs — always masked (e.g. `sk-a*****`)
|
|
269
|
+
- Config file lives in your user data directory, not in project folders
|
|
270
|
+
- No values are sent anywhere other than the `claude` process environment
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Contributing
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
git clone https://github.com/aminechraibi/cc-wrapper
|
|
278
|
+
cd cc-wrapper
|
|
279
|
+
npm install
|
|
280
|
+
npm test
|
|
281
|
+
npm run build
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Refresh Claude env var list from official docs:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npm run sync-envs
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT — [Amine Chraibi](https://github.com/aminechraibi)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { cac } from "cac";
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/commands/new.ts
|
|
8
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
// src/config/store.ts
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
13
|
+
import { dirname } from "path";
|
|
14
|
+
|
|
15
|
+
// src/config/schema.ts
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
var ProfileSchema = z.object({
|
|
18
|
+
env: z.record(z.string(), z.string()),
|
|
19
|
+
args: z.array(z.string())
|
|
20
|
+
});
|
|
21
|
+
var ConfigSchema = z.object({
|
|
22
|
+
default: z.string().nullable(),
|
|
23
|
+
configs: z.record(z.string(), ProfileSchema)
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/config/store.ts
|
|
27
|
+
var EMPTY_CONFIG = { default: null, configs: {} };
|
|
28
|
+
async function readConfig(path) {
|
|
29
|
+
if (!existsSync(path)) return structuredClone(EMPTY_CONFIG);
|
|
30
|
+
const raw = readFileSync(path, "utf-8");
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return ConfigSchema.parse(parsed);
|
|
33
|
+
}
|
|
34
|
+
async function writeConfig(path, config) {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/config/paths.ts
|
|
40
|
+
import envPaths from "env-paths";
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
var paths = envPaths("cc-wrapper", { suffix: "" });
|
|
43
|
+
function getConfigPath() {
|
|
44
|
+
if (process.env["CC_WRAPPER_CONFIG_PATH"]) {
|
|
45
|
+
return process.env["CC_WRAPPER_CONFIG_PATH"];
|
|
46
|
+
}
|
|
47
|
+
return join(paths.config, "config.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/prompts/env-selector.ts
|
|
51
|
+
import { checkbox, input } from "@inquirer/prompts";
|
|
52
|
+
|
|
53
|
+
// src/utils/mask.ts
|
|
54
|
+
var SECRET_PATTERN = /key|token|secret|password|credential/i;
|
|
55
|
+
function maskValue(value) {
|
|
56
|
+
if (value.length <= 4) return "*****";
|
|
57
|
+
return value.slice(0, 4) + "*****";
|
|
58
|
+
}
|
|
59
|
+
function maskEnv(env) {
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(env).map(([k, v]) => [k, SECRET_PATTERN.test(k) ? maskValue(v) : v])
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/prompts/env-selector.ts
|
|
66
|
+
var ENV_EXAMPLES = {
|
|
67
|
+
ANTHROPIC_API_KEY: "sk-ant-api03-...",
|
|
68
|
+
ANTHROPIC_BASE_URL: "http://localhost:8787",
|
|
69
|
+
ANTHROPIC_AUTH_TOKEN: "Bearer sk-ant-...",
|
|
70
|
+
ANTHROPIC_MODEL: "claude-opus-4-7-20250514",
|
|
71
|
+
ANTHROPIC_SMALL_FAST_MODEL: "claude-haiku-4-5-20251001",
|
|
72
|
+
ANTHROPIC_VERTEX_PROJECT_ID: "my-gcp-project-123",
|
|
73
|
+
CLOUD_ML_REGION: "us-central1",
|
|
74
|
+
AWS_REGION: "us-east-1",
|
|
75
|
+
AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE",
|
|
76
|
+
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
77
|
+
HTTP_PROXY: "http://proxy.corp.com:3128",
|
|
78
|
+
HTTPS_PROXY: "http://proxy.corp.com:3128",
|
|
79
|
+
NO_PROXY: "localhost,127.0.0.1",
|
|
80
|
+
BASH_DEFAULT_TIMEOUT_MS: "30000",
|
|
81
|
+
BASH_MAX_TIMEOUT_MS: "120000",
|
|
82
|
+
BASH_MAX_OUTPUT_LENGTH: "50000",
|
|
83
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: "8192",
|
|
84
|
+
MCP_TIMEOUT: "10000",
|
|
85
|
+
MCP_TOOL_TIMEOUT: "60000"
|
|
86
|
+
};
|
|
87
|
+
async function promptEnvSelection(envVars, existing = {}) {
|
|
88
|
+
const selected = await checkbox({
|
|
89
|
+
message: "Select env vars (SPACE to toggle, ENTER to confirm):",
|
|
90
|
+
choices: envVars.map((v, i) => ({
|
|
91
|
+
name: `${String(i + 1).padStart(2)}. ${v.name}${existing[v.name] ? ` [${maskValue(existing[v.name])}]` : ""}`,
|
|
92
|
+
value: v.name,
|
|
93
|
+
checked: v.name in existing,
|
|
94
|
+
description: v.description
|
|
95
|
+
})),
|
|
96
|
+
pageSize: 15
|
|
97
|
+
});
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const name of selected) {
|
|
100
|
+
const current = existing[name];
|
|
101
|
+
const example = ENV_EXAMPLES[name];
|
|
102
|
+
let hint = "";
|
|
103
|
+
if (current) hint = ` (current: ${maskValue(current)}, leave blank to keep)`;
|
|
104
|
+
else if (example) hint = ` (e.g. ${example})`;
|
|
105
|
+
const value = await input({
|
|
106
|
+
message: `${name}${hint}:`,
|
|
107
|
+
required: !current
|
|
108
|
+
});
|
|
109
|
+
result[name] = value || current || "";
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
async function promptArgs(existing = []) {
|
|
114
|
+
const raw = await input({
|
|
115
|
+
message: "Default claude args (space-separated, e.g. --print --model claude-opus-4-7):",
|
|
116
|
+
default: existing.join(" ")
|
|
117
|
+
});
|
|
118
|
+
return raw.trim() ? raw.trim().split(/\s+/) : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/data/env-vars.json
|
|
122
|
+
var env_vars_default = [
|
|
123
|
+
{ name: "ANTHROPIC_API_KEY", description: "Anthropic API key for authentication" },
|
|
124
|
+
{ name: "ANTHROPIC_BASE_URL", description: "Custom base URL for Anthropic API (e.g. local proxy)" },
|
|
125
|
+
{ name: "ANTHROPIC_AUTH_TOKEN", description: "Bearer token for custom auth header" },
|
|
126
|
+
{ name: "CLAUDE_CODE_USE_VERTEX", description: "Use Google Cloud Vertex AI instead of Anthropic API" },
|
|
127
|
+
{ name: "ANTHROPIC_VERTEX_PROJECT_ID", description: "Google Cloud project ID for Vertex AI" },
|
|
128
|
+
{ name: "CLOUD_ML_REGION", description: "Google Cloud region for Vertex AI" },
|
|
129
|
+
{ name: "CLAUDE_CODE_USE_BEDROCK", description: "Use AWS Bedrock instead of Anthropic API" },
|
|
130
|
+
{ name: "AWS_REGION", description: "AWS region for Bedrock" },
|
|
131
|
+
{ name: "AWS_ACCESS_KEY_ID", description: "AWS access key ID for Bedrock auth" },
|
|
132
|
+
{ name: "AWS_SECRET_ACCESS_KEY", description: "AWS secret access key for Bedrock auth" },
|
|
133
|
+
{ name: "AWS_SESSION_TOKEN", description: "AWS session token for Bedrock auth" },
|
|
134
|
+
{ name: "HTTP_PROXY", description: "HTTP proxy URL" },
|
|
135
|
+
{ name: "HTTPS_PROXY", description: "HTTPS proxy URL" },
|
|
136
|
+
{ name: "NO_PROXY", description: "Comma-separated hosts to bypass proxy" },
|
|
137
|
+
{ name: "CLAUDE_CODE_MAX_OUTPUT_TOKENS", description: "Max output tokens for Claude responses" },
|
|
138
|
+
{ name: "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", description: "Disable telemetry and version checks" },
|
|
139
|
+
{ name: "ANTHROPIC_MODEL", description: "Override default Claude model" },
|
|
140
|
+
{ name: "ANTHROPIC_SMALL_FAST_MODEL", description: "Override background task model" },
|
|
141
|
+
{ name: "BASH_DEFAULT_TIMEOUT_MS", description: "Default bash command timeout in milliseconds" },
|
|
142
|
+
{ name: "BASH_MAX_TIMEOUT_MS", description: "Maximum bash command timeout in milliseconds" },
|
|
143
|
+
{ name: "BASH_MAX_OUTPUT_LENGTH", description: "Max characters of bash output before truncation" },
|
|
144
|
+
{ name: "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", description: "TTL for API key helper cache" },
|
|
145
|
+
{ name: "CLAUDE_CODE_SKIP_BEDROCK_AUTH", description: "Skip Bedrock auth validation" },
|
|
146
|
+
{ name: "CLAUDE_CODE_SKIP_VERTEX_AUTH", description: "Skip Vertex auth validation" },
|
|
147
|
+
{ name: "MCP_TIMEOUT", description: "Timeout in ms for MCP server startup" },
|
|
148
|
+
{ name: "MCP_TOOL_TIMEOUT", description: "Timeout in ms for individual MCP tool calls" }
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
// src/commands/new.ts
|
|
152
|
+
async function newCommand() {
|
|
153
|
+
const configPath = getConfigPath();
|
|
154
|
+
const config = await readConfig(configPath);
|
|
155
|
+
const name = await input2({
|
|
156
|
+
message: "Profile name (e.g. local, openrouter, vertex, bedrock):",
|
|
157
|
+
validate: (v) => {
|
|
158
|
+
if (!v.trim()) return "Name required";
|
|
159
|
+
if (config.configs[v.trim()]) return `Profile "${v.trim()}" already exists`;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const profileName = name.trim();
|
|
164
|
+
const env = await promptEnvSelection(env_vars_default);
|
|
165
|
+
const args = await promptArgs();
|
|
166
|
+
config.configs[profileName] = { env, args };
|
|
167
|
+
if (!config.default) {
|
|
168
|
+
config.default = profileName;
|
|
169
|
+
console.log(chalk.dim(`Set "${profileName}" as default (first profile)`));
|
|
170
|
+
}
|
|
171
|
+
await writeConfig(configPath, config);
|
|
172
|
+
console.log(chalk.green(`\u2713 Profile "${profileName}" saved`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/commands/list.ts
|
|
176
|
+
import chalk2 from "chalk";
|
|
177
|
+
async function listCommand() {
|
|
178
|
+
const config = await readConfig(getConfigPath());
|
|
179
|
+
const names = Object.keys(config.configs);
|
|
180
|
+
if (names.length === 0) {
|
|
181
|
+
console.log(chalk2.dim("No profiles. Run `cc-wrapper new` to create one."));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const isDefault = name === config.default;
|
|
186
|
+
const bullet = isDefault ? chalk2.green("\u25CF") : chalk2.dim("\u25CB");
|
|
187
|
+
const label = isDefault ? chalk2.bold(name) + chalk2.dim(" (default)") : name;
|
|
188
|
+
console.log(`${bullet} ${label}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/commands/default.ts
|
|
193
|
+
import chalk3 from "chalk";
|
|
194
|
+
async function defaultCommand(name) {
|
|
195
|
+
const configPath = getConfigPath();
|
|
196
|
+
const config = await readConfig(configPath);
|
|
197
|
+
if (!config.configs[name]) {
|
|
198
|
+
console.error(chalk3.red(`Profile "${name}" not found`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
config.default = name;
|
|
202
|
+
await writeConfig(configPath, config);
|
|
203
|
+
console.log(chalk3.green(`\u2713 Default set to "${name}"`));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/edit.ts
|
|
207
|
+
import chalk4 from "chalk";
|
|
208
|
+
async function editCommand(name) {
|
|
209
|
+
const configPath = getConfigPath();
|
|
210
|
+
const config = await readConfig(configPath);
|
|
211
|
+
if (!config.configs[name]) {
|
|
212
|
+
console.error(chalk4.red(`Profile "${name}" not found`));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const profile = config.configs[name];
|
|
216
|
+
const env = await promptEnvSelection(env_vars_default, profile.env);
|
|
217
|
+
const args = await promptArgs(profile.args);
|
|
218
|
+
config.configs[name] = { env, args };
|
|
219
|
+
await writeConfig(configPath, config);
|
|
220
|
+
console.log(chalk4.green(`\u2713 Profile "${name}" updated`));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/commands/delete.ts
|
|
224
|
+
import { confirm } from "@inquirer/prompts";
|
|
225
|
+
import chalk5 from "chalk";
|
|
226
|
+
async function deleteCommand(name) {
|
|
227
|
+
const configPath = getConfigPath();
|
|
228
|
+
const config = await readConfig(configPath);
|
|
229
|
+
if (!config.configs[name]) {
|
|
230
|
+
console.error(chalk5.red(`Profile "${name}" not found`));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const ok = await confirm({ message: `Delete profile "${name}"?`, default: false });
|
|
234
|
+
if (!ok) {
|
|
235
|
+
console.log(chalk5.dim("Cancelled"));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
delete config.configs[name];
|
|
239
|
+
if (config.default === name) {
|
|
240
|
+
const remaining = Object.keys(config.configs);
|
|
241
|
+
config.default = remaining[0] ?? null;
|
|
242
|
+
if (config.default) {
|
|
243
|
+
console.log(chalk5.dim(`Default changed to "${config.default}"`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await writeConfig(configPath, config);
|
|
247
|
+
console.log(chalk5.green(`\u2713 Profile "${name}" deleted`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/commands/claude.ts
|
|
251
|
+
import chalk6 from "chalk";
|
|
252
|
+
import ora from "ora";
|
|
253
|
+
|
|
254
|
+
// src/utils/spawn.ts
|
|
255
|
+
import spawn from "cross-spawn";
|
|
256
|
+
function spawnClaude(args, env) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const child = spawn("claude", args, {
|
|
259
|
+
stdio: "inherit",
|
|
260
|
+
shell: true,
|
|
261
|
+
env
|
|
262
|
+
});
|
|
263
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
264
|
+
child.on("error", reject);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/commands/claude.ts
|
|
269
|
+
var DANGEROUS = "--dangerously-skip-permissions";
|
|
270
|
+
function buildClaudeArgs(profileArgs, extraArgs, disableDangerous) {
|
|
271
|
+
const base = profileArgs.filter((a) => a !== DANGEROUS);
|
|
272
|
+
if (!disableDangerous) base.unshift(DANGEROUS);
|
|
273
|
+
return [...base, ...extraArgs];
|
|
274
|
+
}
|
|
275
|
+
async function claudeCommand(extraArgs, options) {
|
|
276
|
+
const configPath = getConfigPath();
|
|
277
|
+
const config = await readConfig(configPath);
|
|
278
|
+
const profileName = options.profile ?? config.default;
|
|
279
|
+
if (!profileName) {
|
|
280
|
+
console.error(chalk6.red("No default profile set. Run `cc-wrapper new` or `cc-wrapper default <name>`."));
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
const profile = config.configs[profileName];
|
|
284
|
+
if (!profile) {
|
|
285
|
+
console.error(chalk6.red(`Profile "${profileName}" not found`));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const args = buildClaudeArgs(profile.args, extraArgs, !!options.dd);
|
|
289
|
+
const env = { ...process.env, ...profile.env };
|
|
290
|
+
const spinner = ora(`Launching claude (profile: ${profileName})`).start();
|
|
291
|
+
console.log(chalk6.dim("env: " + JSON.stringify(maskEnv(profile.env))));
|
|
292
|
+
spinner.stop();
|
|
293
|
+
const code = await spawnClaude(args, env);
|
|
294
|
+
process.exit(code);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/cli.ts
|
|
298
|
+
process.on("SIGINT", () => {
|
|
299
|
+
process.stdout.write("\n");
|
|
300
|
+
process.exit(0);
|
|
301
|
+
});
|
|
302
|
+
process.on("uncaughtException", (err) => {
|
|
303
|
+
if (err.name === "ExitPromptError") {
|
|
304
|
+
process.stdout.write("\n");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
console.error(chalk7.red(`Error: ${err.message}`));
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
|
310
|
+
process.on("unhandledRejection", (reason) => {
|
|
311
|
+
if (reason instanceof Error && reason.name === "ExitPromptError") {
|
|
312
|
+
process.stdout.write("\n");
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
console.error(chalk7.red(`Error: ${String(reason)}`));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
318
|
+
var cli = cac("cc-wrapper");
|
|
319
|
+
cli.command("new", "Create a new profile").action(newCommand);
|
|
320
|
+
cli.command("list", "List all profiles").action(listCommand);
|
|
321
|
+
cli.command("default <name>", "Set default profile").action(defaultCommand);
|
|
322
|
+
cli.command("edit <name>", "Edit a profile interactively").action(editCommand);
|
|
323
|
+
cli.command("delete <name>", "Delete a profile").action(deleteCommand);
|
|
324
|
+
cli.command("claude [...args]", "Run claude with the default profile").option("--dd", "Disable --dangerously-skip-permissions injection").option("-p, --profile <name>", "Use a specific profile instead of default").action((args, options) => {
|
|
325
|
+
return claudeCommand(args, options);
|
|
326
|
+
});
|
|
327
|
+
cli.help();
|
|
328
|
+
cli.version("0.1.0");
|
|
329
|
+
cli.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-wrapper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-platform CLI wrapper for claude with reusable environment profiles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-wrapper": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"start": "node dist/cli.js",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"sync-envs": "npx tsx scripts/sync-envs.ts",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["claude", "anthropic", "cli", "wrapper", "profiles"],
|
|
23
|
+
"author": "Amine Chraibi <aminechraibi2020@gmail.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/aminechraibi/cc-wrapper"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/aminechraibi/cc-wrapper#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/aminechraibi/cc-wrapper/issues"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@inquirer/prompts": "^7.5.0",
|
|
38
|
+
"cac": "^6.7.14",
|
|
39
|
+
"chalk": "^5.4.1",
|
|
40
|
+
"cross-spawn": "^7.0.6",
|
|
41
|
+
"env-paths": "^3.0.0",
|
|
42
|
+
"ora": "^8.2.0",
|
|
43
|
+
"zod": "^3.24.3"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/cross-spawn": "^6.0.6",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"tsx": "^4.19.4",
|
|
49
|
+
"tsup": "^8.5.0",
|
|
50
|
+
"typescript": "^5.8.3",
|
|
51
|
+
"vitest": "^3.1.4"
|
|
52
|
+
}
|
|
53
|
+
}
|