ccproxypal 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 ADDED
@@ -0,0 +1,105 @@
1
+ # ccproxypal
2
+
3
+ Local proxy server that routes AI API requests through your Claude Code OAuth subscription.
4
+
5
+ Zero dependencies. Node.js 18+ only.
6
+
7
+ ## Commands
8
+
9
+ ### `token` — Print OAuth tokens
10
+
11
+ ```bash
12
+ npx ccproxypal token
13
+ ```
14
+
15
+ Output:
16
+ ```json
17
+ { "accessToken": "sk-ant-oau01-...", "refreshToken": "..." }
18
+ ```
19
+
20
+ ---
21
+
22
+ ### `serve` — Start the proxy server
23
+
24
+ **Host mode** (uses local Claude credentials):
25
+ ```bash
26
+ npx ccproxypal serve
27
+ npx ccproxypal serve --tunnel # + Cloudflare public URL
28
+ npx ccproxypal serve --port 9000
29
+ ```
30
+
31
+ **Client mode** (tokens provided externally — no local credentials needed):
32
+ ```bash
33
+ npx ccproxypal serve \
34
+ --access-token sk-ant-oau01-... \
35
+ --refresh-token ...
36
+ ```
37
+
38
+ With tunnel:
39
+ ```bash
40
+ npx ccproxypal serve \
41
+ --access-token sk-ant-... \
42
+ --refresh-token ... \
43
+ --tunnel
44
+ ```
45
+
46
+ ---
47
+
48
+ ### `configure` — Configure tools to use the proxy
49
+
50
+ ```bash
51
+ # List available tools
52
+ npx ccproxypal configure
53
+
54
+ # Configure (writes ANTHROPIC_BASE_URL to tool's config file)
55
+ npx ccproxypal configure claude-code
56
+ npx ccproxypal configure opencode
57
+
58
+ # Custom URL (e.g. tunnel)
59
+ npx ccproxypal configure claude-code --url https://xxxx.trycloudflare.com
60
+
61
+ # Custom port
62
+ npx ccproxypal configure claude-code --port 9000
63
+
64
+ # Remove config
65
+ npx ccproxypal configure remove claude-code
66
+ npx ccproxypal configure remove opencode
67
+ ```
68
+
69
+ **Supported tools:**
70
+
71
+ | Tool | Config file |
72
+ |------|-------------|
73
+ | `claude-code` | `~/.claude/settings.json` |
74
+ | `opencode` | `~/.config/opencode/config.json` |
75
+
76
+ For **Cursor**: copy the proxy URL and paste into *Settings → Models → API Base URL*.
77
+
78
+ ---
79
+
80
+ ## Proxy Endpoints
81
+
82
+ | Method | Path | Description |
83
+ |--------|------|-------------|
84
+ | `POST` | `/v1/messages` | Anthropic Messages API |
85
+ | `POST` | `/v1/chat/completions` | OpenAI Chat Completions (auto-converted) |
86
+ | `GET` | `/v1/models` | Live model list from Anthropic |
87
+ | `GET` | `/health` | Health + token status |
88
+
89
+ ## Prerequisites
90
+
91
+ ```bash
92
+ # Host mode: authenticate with Claude CLI
93
+ claude auth login
94
+
95
+ # Tunnel support
96
+ brew install cloudflared
97
+ ```
98
+
99
+ ## Publish
100
+
101
+ ```bash
102
+ cd npm-pkg
103
+ npm login
104
+ npm publish
105
+ ```
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getToken, setManualToken } from '../src/token.js';
4
+ import { createServer } from '../src/server.js';
5
+ import { startTunnel, isCloudflaredAvailable } from '../src/tunnel.js';
6
+ import { configureTool, removeToolConfig, listTools } from '../src/configure.js';
7
+
8
+ // ─── Argument parsing ─────────────────────────────────────────────────────────
9
+
10
+ function parseArgs(argv) {
11
+ const opts = {
12
+ port: 8082,
13
+ tunnel: false,
14
+ accessToken: null,
15
+ refreshToken: null,
16
+ };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if ((a === '--port' || a === '-p') && argv[i + 1]) opts.port = parseInt(argv[++i], 10);
20
+ else if (a === '--tunnel' || a === '-t') opts.tunnel = true;
21
+ else if ((a === '--access-token' || a === '-a') && argv[i + 1]) opts.accessToken = argv[++i];
22
+ else if ((a === '--refresh-token' || a === '-r') && argv[i + 1]) opts.refreshToken = argv[++i];
23
+ }
24
+ return opts;
25
+ }
26
+
27
+ // ─── Commands ─────────────────────────────────────────────────────────────────
28
+
29
+ async function cmdToken() {
30
+ const token = await getToken();
31
+ process.stdout.write(JSON.stringify(token, null, 2) + '\n');
32
+ }
33
+
34
+ async function cmdServe(argv) {
35
+ const { port, tunnel, accessToken, refreshToken } = parseArgs(argv);
36
+
37
+ // Client mode: inject tokens manually
38
+ if (accessToken || refreshToken) {
39
+ if (!accessToken || !refreshToken) {
40
+ process.stderr.write('Error: --access-token and --refresh-token must both be provided.\n');
41
+ process.exit(1);
42
+ }
43
+ setManualToken(accessToken, refreshToken);
44
+ process.stdout.write('Using provided tokens (client mode).\n');
45
+ } else {
46
+ // Host mode: validate local credentials before starting
47
+ try {
48
+ await getToken();
49
+ } catch (err) {
50
+ process.stderr.write(`Error: ${err.message}\n`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ if (tunnel && !isCloudflaredAvailable()) {
56
+ process.stderr.write('Error: cloudflared not found.\nInstall with: brew install cloudflared\n');
57
+ process.exit(1);
58
+ }
59
+
60
+ const server = createServer(port);
61
+
62
+ if (tunnel) {
63
+ startTunnel(port, {
64
+ onError: (err) => process.stderr.write(`Tunnel: ${err.message}\n`),
65
+ });
66
+ }
67
+
68
+ function shutdown() {
69
+ process.stdout.write('\nShutting down...\n');
70
+ server.close(() => process.exit(0));
71
+ setTimeout(() => process.exit(0), 3000);
72
+ }
73
+ process.on('SIGINT', shutdown);
74
+ process.on('SIGTERM', shutdown);
75
+ }
76
+
77
+ async function cmdConfigure(argv) {
78
+ const [sub, toolId] = argv;
79
+
80
+ if (!sub) {
81
+ // List tools
82
+ process.stdout.write('Available tools:\n');
83
+ for (const t of listTools()) {
84
+ process.stdout.write(` ${t.id.padEnd(14)} ${t.name} → ${t.path}\n`);
85
+ }
86
+ process.stdout.write('\nUsage:\n');
87
+ process.stdout.write(' ccproxypal configure <tool> [--url <proxy_url>]\n');
88
+ process.stdout.write(' ccproxypal configure remove <tool>\n');
89
+ return;
90
+ }
91
+
92
+ if (sub === 'remove') {
93
+ if (!toolId) {
94
+ process.stderr.write('Usage: ccproxypal configure remove <tool>\n');
95
+ process.exit(1);
96
+ }
97
+ await removeToolConfig(toolId);
98
+ return;
99
+ }
100
+
101
+ // sub is the tool id; look for --url flag
102
+ let proxyUrl = 'http://localhost:8082';
103
+ const urlIdx = argv.indexOf('--url');
104
+ if (urlIdx !== -1 && argv[urlIdx + 1]) proxyUrl = argv[urlIdx + 1];
105
+
106
+ const portIdx = argv.indexOf('--port');
107
+ if (portIdx !== -1 && argv[portIdx + 1]) {
108
+ proxyUrl = `http://localhost:${argv[portIdx + 1]}`;
109
+ }
110
+
111
+ await configureTool(sub, proxyUrl);
112
+ }
113
+
114
+ function cmdHelp() {
115
+ process.stdout.write(
116
+ [
117
+ 'Usage: ccproxypal <command> [options]',
118
+ '',
119
+ 'Commands:',
120
+ ' token Print Claude OAuth tokens as JSON',
121
+ '',
122
+ ' serve Start proxy (uses local Claude credentials)',
123
+ ' serve --tunnel Start proxy + Cloudflare tunnel',
124
+ ' serve --port <port> Custom port (default: 8082)',
125
+ ' serve --access-token <token> Client mode: use provided tokens',
126
+ ' --refresh-token <token>',
127
+ '',
128
+ ' configure List configurable tools',
129
+ ' configure <tool> [--url <url>] Write proxy URL to tool config',
130
+ ' configure <tool> --port <port> Write http://localhost:<port> to tool config',
131
+ ' configure remove <tool> Remove proxy config from tool',
132
+ '',
133
+ 'Tools:',
134
+ ' claude-code ~/.claude/settings.json',
135
+ ' opencode ~/.config/opencode/config.json',
136
+ '',
137
+ 'Examples:',
138
+ ' npx ccproxypal token',
139
+ ' npx ccproxypal serve --tunnel',
140
+ ' npx ccproxypal serve --access-token sk-ant-... --refresh-token ...',
141
+ ' npx ccproxypal configure claude-code',
142
+ ' npx ccproxypal configure claude-code --url https://xxxx.trycloudflare.com',
143
+ ' npx ccproxypal configure remove claude-code',
144
+ '',
145
+ ].join('\n')
146
+ );
147
+ }
148
+
149
+ // ─── Dispatch ─────────────────────────────────────────────────────────────────
150
+
151
+ const [, , cmd, ...rest] = process.argv;
152
+
153
+ const dispatch = {
154
+ token: () => cmdToken().catch(bail),
155
+ serve: () => cmdServe(rest).catch(bail),
156
+ configure: () => cmdConfigure(rest).catch(bail),
157
+ help: () => cmdHelp(),
158
+ '--help': () => cmdHelp(),
159
+ '-h': () => cmdHelp(),
160
+ };
161
+
162
+ function bail(err) {
163
+ process.stderr.write(`Error: ${err.message}\n`);
164
+ process.exit(1);
165
+ }
166
+
167
+ if (cmd && dispatch[cmd]) {
168
+ dispatch[cmd]();
169
+ } else {
170
+ cmdHelp();
171
+ process.exit(cmd ? 1 : 0);
172
+ }