cloding 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/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/cloding.js +871 -0
- package/models.json +44 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Carlos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ⚡ cloding
|
|
2
|
+
|
|
3
|
+
**Claude Code with any model via OpenRouter.**
|
|
4
|
+
|
|
5
|
+
Claude Code costs $5/$25 per Mtok. Qwen 3 Coder costs $0.07/$0.30. That's 71x cheaper on input, 83x cheaper on output. You'd be an idiot not to use it at scale.
|
|
6
|
+
|
|
7
|
+
Cloding lets you run Claude Code — tools, file editing, terminal access, the whole thing — with any OpenRouter model. Same experience, fraction of the cost. Zero dependencies, zero overhead. It sets 4 env vars and spawns `claude`. That's it.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install -g cloding
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export OPENROUTER_API_KEY=sk-or-v1-your-key-here
|
|
17
|
+
cloding
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You're now running Claude Code with Qwen 3 Coder at **$0.07/Mtok input** instead of $5/Mtok.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cloding # Interactive session with Qwen (default)
|
|
26
|
+
cloding -m haiku # Use Claude Haiku 4.5
|
|
27
|
+
cloding -m sonnet # Use Claude Sonnet 4
|
|
28
|
+
cloding -m opus # Use Claude Opus 4.6
|
|
29
|
+
cloding -m deepseek # Use DeepSeek Coder V3
|
|
30
|
+
cloding -p "fix the login bug" # Non-interactive, single prompt
|
|
31
|
+
cloding --list-models # Show all models with pricing
|
|
32
|
+
cloding -m meta-llama/llama-4-scout # Any OpenRouter model ID works
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All Claude Code flags pass through:
|
|
36
|
+
```bash
|
|
37
|
+
cloding --allowedTools Read,Write,Bash
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Models & Cost
|
|
41
|
+
|
|
42
|
+
| Shortcut | Model | Input $/Mtok | Output $/Mtok | vs Claude Code |
|
|
43
|
+
|----------|-------|-------------|---------------|----------------|
|
|
44
|
+
| `qwen` | Qwen 3 Coder | $0.07 | $0.30 | **71x cheaper** |
|
|
45
|
+
| `deepseek` | DeepSeek Coder V3 | $0.14 | $0.28 | **36x cheaper** |
|
|
46
|
+
| `haiku` | Claude Haiku 4.5 | $0.80 | $4.00 | 6x cheaper |
|
|
47
|
+
| `gemini` | Gemini 2.5 Pro | $1.25 | $10.00 | 4x cheaper |
|
|
48
|
+
| `sonnet` | Claude Sonnet 4 | $3.00 | $15.00 | 1.7x cheaper |
|
|
49
|
+
| `opus` | Claude Opus 4.6 | $15.00 | $75.00 | 3x more expensive |
|
|
50
|
+
|
|
51
|
+
> A 30-minute coding session that costs ~$5 with Claude Code costs ~$0.07 with Qwen. Same tools, same workflow.
|
|
52
|
+
|
|
53
|
+
## Docker Mode
|
|
54
|
+
|
|
55
|
+
When you run Claude Code, it has full access to your machine — your files, your terminal, your `.env`, your SSH keys, everything. It's an LLM agent with root-level power and nothing about that is secure. Nobody seems to care that these models are looking at all your stuff and running wild.
|
|
56
|
+
|
|
57
|
+
Docker mode puts it in a box. The model can only touch the workspace you mount and nothing else. It can't read your secrets, wreck your system, or do anything outside the container. Non-root user, no access to your host filesystem, network isolated.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cloding docker build # Build image (one-time)
|
|
61
|
+
cloding docker shell # Interactive session
|
|
62
|
+
cloding docker run "fix the bug" # Run a prompt
|
|
63
|
+
cloding docker run -m haiku "prompt" # Specific model
|
|
64
|
+
cloding docker run -w ./myproject # Mount workspace
|
|
65
|
+
cloding docker run --memory 4g --cpus 2 # Resource limits
|
|
66
|
+
cloding docker status # Show running containers
|
|
67
|
+
cloding docker stop # Stop all containers
|
|
68
|
+
cloding docker clean # Remove stopped containers
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Your workspace gets mounted read-write at `/workspace` inside the container. That's the only thing the model can touch.
|
|
72
|
+
|
|
73
|
+
## Pipeline Mode
|
|
74
|
+
|
|
75
|
+
Multi-stage coding pipeline: Plan → Explore → Code → Review, with parallel fan-out. Assign different models to different stages — Opus for planning, Qwen for coding.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd pipeline && pip install -e . # Requires Python 3.11+
|
|
79
|
+
cloding pipeline "Add auth" --workspace ./myapp --no-docker
|
|
80
|
+
cloding pipeline -c configs/qwen-fanout.yaml "Refactor the DB layer"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
8 pipeline configs included: default, quick, fan-out, opus-plan+qwen-code, human-in-the-loop, and more.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export OPENROUTER_API_KEY=sk-or-v1-your-key-here # Required
|
|
89
|
+
export CLODING_DEFAULT_MODEL=qwen # Optional (default: qwen)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Add custom model shortcuts by editing `models.json`.
|
|
93
|
+
|
|
94
|
+
## Prerequisites
|
|
95
|
+
|
|
96
|
+
- **Node.js 18+**
|
|
97
|
+
- **Claude Code**: `npm install -g @anthropic-ai/claude-code`
|
|
98
|
+
- **OpenRouter API key**: [openrouter.ai/keys](https://openrouter.ai/keys)
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/bin/cloding.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cloding — Claude Code with any model via OpenRouter.
|
|
5
|
+
*
|
|
6
|
+
* Sets the right env vars and spawns `claude` so you can use
|
|
7
|
+
* Qwen, Haiku, Sonnet, or any OpenRouter model at a fraction of the cost.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* cloding # Interactive with default model (Qwen)
|
|
11
|
+
* cloding -m haiku # Use a different model
|
|
12
|
+
* cloding -p "fix the bug" # Non-interactive single prompt
|
|
13
|
+
* cloding --list-models # Show available models
|
|
14
|
+
* cloding pipeline "Add auth" # Run the full pipeline
|
|
15
|
+
* cloding docker build # Build Docker image
|
|
16
|
+
* cloding docker run "prompt" # Run in a Docker container
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { spawn, execSync } = require("child_process");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
// .env loader (no dependencies)
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
function loadEnvFile() {
|
|
27
|
+
// Search for .env in: cwd, then cloding package root
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(process.cwd(), ".env"),
|
|
30
|
+
path.join(__dirname, "..", ".env"),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const envPath of candidates) {
|
|
34
|
+
if (fs.existsSync(envPath)) {
|
|
35
|
+
const lines = fs.readFileSync(envPath, "utf8").split("\n");
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
39
|
+
const eqIdx = trimmed.indexOf("=");
|
|
40
|
+
if (eqIdx === -1) continue;
|
|
41
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
42
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
43
|
+
// Strip surrounding quotes
|
|
44
|
+
if (
|
|
45
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
46
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
47
|
+
) {
|
|
48
|
+
val = val.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
// Don't override existing env vars
|
|
51
|
+
if (!process.env[key]) {
|
|
52
|
+
process.env[key] = val;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return envPath;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ──────────────────────────────────────────────
|
|
62
|
+
// Model registry
|
|
63
|
+
// ──────────────────────────────────────────────
|
|
64
|
+
function loadModels() {
|
|
65
|
+
const modelsPath = path.join(__dirname, "..", "models.json");
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(modelsPath, "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
console.error("Error: Could not load models.json");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ──────────────────────────────────────────────
|
|
75
|
+
// Arg parsing (lightweight, no dependencies)
|
|
76
|
+
// ──────────────────────────────────────────────
|
|
77
|
+
function parseArgs(argv) {
|
|
78
|
+
const args = {
|
|
79
|
+
model: null,
|
|
80
|
+
prompt: null,
|
|
81
|
+
listModels: false,
|
|
82
|
+
version: false,
|
|
83
|
+
help: false,
|
|
84
|
+
pipeline: false,
|
|
85
|
+
pipelineArgs: [],
|
|
86
|
+
docker: false,
|
|
87
|
+
dockerSubcommand: null,
|
|
88
|
+
dockerArgs: [],
|
|
89
|
+
claudeArgs: [], // passthrough args for claude CLI
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < argv.length) {
|
|
94
|
+
const arg = argv[i];
|
|
95
|
+
|
|
96
|
+
if (arg === "pipeline") {
|
|
97
|
+
args.pipeline = true;
|
|
98
|
+
args.pipelineArgs = argv.slice(i + 1);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (arg === "docker") {
|
|
103
|
+
args.docker = true;
|
|
104
|
+
args.dockerSubcommand = argv[i + 1] || "help";
|
|
105
|
+
args.dockerArgs = argv.slice(i + 2);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (arg) {
|
|
110
|
+
case "-m":
|
|
111
|
+
case "--model":
|
|
112
|
+
if (i + 1 >= argv.length) {
|
|
113
|
+
console.error("Error: --model requires a value.\n Usage: cloding -m <model>");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
args.model = argv[++i];
|
|
117
|
+
break;
|
|
118
|
+
case "-p":
|
|
119
|
+
case "--prompt":
|
|
120
|
+
if (i + 1 >= argv.length) {
|
|
121
|
+
console.error("Error: --prompt requires a value.\n Usage: cloding -p \"your prompt\"");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
args.prompt = argv[++i];
|
|
125
|
+
break;
|
|
126
|
+
case "--list-models":
|
|
127
|
+
args.listModels = true;
|
|
128
|
+
break;
|
|
129
|
+
case "-v":
|
|
130
|
+
case "--version":
|
|
131
|
+
args.version = true;
|
|
132
|
+
break;
|
|
133
|
+
case "-h":
|
|
134
|
+
case "--help":
|
|
135
|
+
args.help = true;
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
args.claudeArgs.push(arg);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return args;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ──────────────────────────────────────────────
|
|
148
|
+
// Display helpers
|
|
149
|
+
// ──────────────────────────────────────────────
|
|
150
|
+
function printVersion() {
|
|
151
|
+
const pkg = JSON.parse(
|
|
152
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
|
|
153
|
+
);
|
|
154
|
+
console.log(`cloding v${pkg.version}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printHelp() {
|
|
158
|
+
console.log(`
|
|
159
|
+
cloding — Claude Code with any model via OpenRouter
|
|
160
|
+
|
|
161
|
+
USAGE:
|
|
162
|
+
cloding Interactive session (default: Qwen)
|
|
163
|
+
cloding -m haiku Use a specific model
|
|
164
|
+
cloding -p "fix the bug" Non-interactive single prompt
|
|
165
|
+
cloding --list-models Show available models and costs
|
|
166
|
+
cloding pipeline "Add auth" Run the full pipeline (requires Python)
|
|
167
|
+
cloding docker <command> Docker container management
|
|
168
|
+
|
|
169
|
+
OPTIONS:
|
|
170
|
+
-m, --model <name|id> Model shortcut or full OpenRouter model ID
|
|
171
|
+
-p, --prompt <text> Run non-interactively with a single prompt
|
|
172
|
+
--list-models Show available models with pricing
|
|
173
|
+
-v, --version Show version
|
|
174
|
+
-h, --help Show this help
|
|
175
|
+
|
|
176
|
+
DOCKER COMMANDS:
|
|
177
|
+
cloding docker build Build the cloding Docker image
|
|
178
|
+
cloding docker run "prompt" Run a prompt in a container
|
|
179
|
+
cloding docker shell Interactive claude session in a container
|
|
180
|
+
cloding docker status Show running cloding containers
|
|
181
|
+
cloding docker stop Stop all cloding containers
|
|
182
|
+
cloding docker clean Remove stopped cloding containers
|
|
183
|
+
cloding docker help Show Docker help
|
|
184
|
+
|
|
185
|
+
MODELS (shortcuts):
|
|
186
|
+
qwen Qwen 3 Coder $0.07/$0.30 per Mtok (default, ~250x cheaper)
|
|
187
|
+
haiku Claude Haiku 4.5 $0.80/$4.00 per Mtok
|
|
188
|
+
sonnet Claude Sonnet 4 $3.00/$15.00 per Mtok
|
|
189
|
+
opus Claude Opus 4.6 $15.00/$75.00 per Mtok
|
|
190
|
+
deepseek DeepSeek Coder V3 $0.14/$0.28 per Mtok
|
|
191
|
+
gemini Gemini 2.5 Pro $1.25/$10.00 per Mtok
|
|
192
|
+
|
|
193
|
+
Or pass any OpenRouter model ID:
|
|
194
|
+
cloding -m meta-llama/llama-4-scout
|
|
195
|
+
|
|
196
|
+
ENVIRONMENT:
|
|
197
|
+
OPENROUTER_API_KEY Required. Your OpenRouter API key.
|
|
198
|
+
CLODING_DEFAULT_MODEL Optional. Default model shortcut (default: qwen).
|
|
199
|
+
|
|
200
|
+
EXAMPLES:
|
|
201
|
+
cloding # Start coding with Qwen ($0.07/Mtok)
|
|
202
|
+
cloding -m haiku # Quick task with Haiku
|
|
203
|
+
cloding -m opus -p "Review arch" # One-shot with Opus
|
|
204
|
+
cloding docker build # Build Docker image first
|
|
205
|
+
cloding docker run "Fix the bug" # Run isolated in Docker
|
|
206
|
+
cloding docker shell # Interactive Docker session
|
|
207
|
+
cloding pipeline "Add auth" -c configs/qwen-fanout.yaml
|
|
208
|
+
|
|
209
|
+
All other arguments are passed through to claude.
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function printModels(models) {
|
|
214
|
+
console.log("\nAvailable models:\n");
|
|
215
|
+
console.log(
|
|
216
|
+
" Shortcut Model Input $/Mtok Output $/Mtok vs Opus"
|
|
217
|
+
);
|
|
218
|
+
console.log(
|
|
219
|
+
" ─────────── ──────────────────────── ────────────── ────────────── ───────"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Opus cost for comparison
|
|
223
|
+
const opusOut = models.opus ? models.opus.out : 75.0;
|
|
224
|
+
|
|
225
|
+
for (const [shortcut, m] of Object.entries(models)) {
|
|
226
|
+
const savings = opusOut / m.out;
|
|
227
|
+
const savingsStr =
|
|
228
|
+
shortcut === "opus" ? " baseline" : ` ${savings.toFixed(0)}x cheaper`;
|
|
229
|
+
console.log(
|
|
230
|
+
` ${shortcut.padEnd(11)} ${m.name.padEnd(24)} $${m.in.toFixed(2).padStart(6)} $${m.out.toFixed(2).padStart(6)}${savingsStr}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
"\n Use: cloding -m <shortcut> or cloding -m <openrouter-model-id>\n"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ──────────────────────────────────────────────
|
|
240
|
+
// Resolve model to OpenRouter model ID
|
|
241
|
+
// ──────────────────────────────────────────────
|
|
242
|
+
function resolveModel(modelArg, models) {
|
|
243
|
+
if (!modelArg) {
|
|
244
|
+
// Use default from env, or fall back to qwen
|
|
245
|
+
const defaultModel = process.env.CLODING_DEFAULT_MODEL || "qwen";
|
|
246
|
+
return resolveModel(defaultModel, models);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check shortcuts first
|
|
250
|
+
if (models[modelArg.toLowerCase()]) {
|
|
251
|
+
return models[modelArg.toLowerCase()];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Assume it's a full OpenRouter model ID (e.g. "meta-llama/llama-4-scout")
|
|
255
|
+
return {
|
|
256
|
+
id: modelArg,
|
|
257
|
+
name: modelArg,
|
|
258
|
+
in: 0,
|
|
259
|
+
out: 0,
|
|
260
|
+
description: "Custom model",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ──────────────────────────────────────────────
|
|
265
|
+
// Docker helpers
|
|
266
|
+
// ──────────────────────────────────────────────
|
|
267
|
+
const DOCKER_IMAGE = "cloding:latest";
|
|
268
|
+
const DOCKER_NETWORK = "cloding-net";
|
|
269
|
+
|
|
270
|
+
function dockerAvailable() {
|
|
271
|
+
try {
|
|
272
|
+
execSync("docker --version", { stdio: "ignore" });
|
|
273
|
+
return true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function dockerImageExists() {
|
|
280
|
+
try {
|
|
281
|
+
// Safe: no user input in this command, just a constant image name
|
|
282
|
+
const result = execSync(`docker images -q ${DOCKER_IMAGE}`, {
|
|
283
|
+
encoding: "utf8",
|
|
284
|
+
});
|
|
285
|
+
return result.trim().length > 0;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getDockerfilePath() {
|
|
292
|
+
// Check relative to package: pipeline/docker/Dockerfile
|
|
293
|
+
const bundled = path.join(__dirname, "..", "pipeline", "docker");
|
|
294
|
+
if (fs.existsSync(path.join(bundled, "Dockerfile"))) {
|
|
295
|
+
return bundled;
|
|
296
|
+
}
|
|
297
|
+
// Check cwd
|
|
298
|
+
const cwdDocker = path.join(process.cwd(), "pipeline", "docker");
|
|
299
|
+
if (fs.existsSync(path.join(cwdDocker, "Dockerfile"))) {
|
|
300
|
+
return cwdDocker;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ensureNetwork() {
|
|
306
|
+
try {
|
|
307
|
+
// Safe: constant network name, no user input. Uses spawnSync for safety.
|
|
308
|
+
const result = require("child_process").spawnSync(
|
|
309
|
+
"docker", ["network", "create", DOCKER_NETWORK],
|
|
310
|
+
{ stdio: "ignore" }
|
|
311
|
+
);
|
|
312
|
+
// Ignore errors — network may already exist
|
|
313
|
+
} catch {
|
|
314
|
+
// Network already exists, that's fine
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function printDockerHelp() {
|
|
319
|
+
console.log(`
|
|
320
|
+
cloding docker — Run Claude Code in isolated Docker containers
|
|
321
|
+
|
|
322
|
+
COMMANDS:
|
|
323
|
+
cloding docker build Build the cloding Docker image
|
|
324
|
+
cloding docker run "your prompt here" Run a prompt in a fresh container
|
|
325
|
+
cloding docker run -m haiku "prompt" Run with a specific model
|
|
326
|
+
cloding docker shell Interactive claude session in Docker
|
|
327
|
+
cloding docker shell -m sonnet Interactive with specific model
|
|
328
|
+
cloding docker status Show running cloding containers
|
|
329
|
+
cloding docker stop Stop all running cloding containers
|
|
330
|
+
cloding docker clean Remove all stopped cloding containers
|
|
331
|
+
cloding docker help Show this help
|
|
332
|
+
|
|
333
|
+
OPTIONS (for run/shell):
|
|
334
|
+
-m, --model <name> Model shortcut or OpenRouter ID (default: qwen)
|
|
335
|
+
-w, --workspace <path> Mount a local directory as /workspace (default: cwd)
|
|
336
|
+
--memory <limit> Container memory limit (default: 2g)
|
|
337
|
+
--cpus <limit> Container CPU limit (default: 1.0)
|
|
338
|
+
--name <name> Custom container name
|
|
339
|
+
--no-rm Don't auto-remove container on exit
|
|
340
|
+
|
|
341
|
+
EXAMPLES:
|
|
342
|
+
cloding docker build
|
|
343
|
+
cloding docker run "Add error handling to src/api.js"
|
|
344
|
+
cloding docker run -m haiku -w ./myproject "Fix the tests"
|
|
345
|
+
cloding docker shell -w /home/user/code
|
|
346
|
+
cloding docker status
|
|
347
|
+
cloding docker stop
|
|
348
|
+
|
|
349
|
+
NOTES:
|
|
350
|
+
- Each container gets its own isolated environment
|
|
351
|
+
- Workspace is mounted at /workspace inside the container
|
|
352
|
+
- Containers auto-remove on exit (use --no-rm to keep them)
|
|
353
|
+
- Containers run as non-root user 'coder' for security
|
|
354
|
+
- Resource limits: 2GB RAM, 1 CPU core (configurable)
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function dockerBuild() {
|
|
359
|
+
const dockerDir = getDockerfilePath();
|
|
360
|
+
if (!dockerDir) {
|
|
361
|
+
console.error(
|
|
362
|
+
"Error: Dockerfile not found.\n\n" +
|
|
363
|
+
"Expected at: pipeline/docker/Dockerfile\n" +
|
|
364
|
+
"Make sure you have the full cloding package with Docker support.\n"
|
|
365
|
+
);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(`\x1b[36m⚡ cloding\x1b[0m Building Docker image: ${DOCKER_IMAGE}`);
|
|
370
|
+
console.log(` Dockerfile: ${path.join(dockerDir, "Dockerfile")}\n`);
|
|
371
|
+
|
|
372
|
+
const child = spawn("docker", ["build", "-t", DOCKER_IMAGE, dockerDir], {
|
|
373
|
+
stdio: "inherit",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
child.on("exit", (code) => {
|
|
377
|
+
if (code === 0) {
|
|
378
|
+
console.log(
|
|
379
|
+
`\n\x1b[32m✓ Image ${DOCKER_IMAGE} built successfully!\x1b[0m`
|
|
380
|
+
);
|
|
381
|
+
console.log(" Run: cloding docker shell");
|
|
382
|
+
console.log(' Run: cloding docker run "your prompt here"\n');
|
|
383
|
+
} else {
|
|
384
|
+
console.error(`\n\x1b[31m✗ Build failed (exit code ${code})\x1b[0m`);
|
|
385
|
+
}
|
|
386
|
+
process.exit(code ?? 0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
child.on("error", (err) => {
|
|
390
|
+
console.error(`Error: ${err.message}`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function dockerRun(dockerArgs, models, interactive) {
|
|
396
|
+
// Parse docker run/shell args
|
|
397
|
+
let modelArg = null;
|
|
398
|
+
let workspace = process.cwd();
|
|
399
|
+
let memory = "2g";
|
|
400
|
+
let cpus = "1.0";
|
|
401
|
+
let containerName = null;
|
|
402
|
+
let autoRemove = true;
|
|
403
|
+
let prompt = null;
|
|
404
|
+
const extraClaudeArgs = [];
|
|
405
|
+
|
|
406
|
+
let i = 0;
|
|
407
|
+
while (i < dockerArgs.length) {
|
|
408
|
+
const arg = dockerArgs[i];
|
|
409
|
+
switch (arg) {
|
|
410
|
+
case "-m":
|
|
411
|
+
case "--model":
|
|
412
|
+
if (i + 1 >= dockerArgs.length) { console.error("Error: --model requires a value."); process.exit(1); }
|
|
413
|
+
modelArg = dockerArgs[++i];
|
|
414
|
+
break;
|
|
415
|
+
case "-w":
|
|
416
|
+
case "--workspace":
|
|
417
|
+
if (i + 1 >= dockerArgs.length) { console.error("Error: --workspace requires a path."); process.exit(1); }
|
|
418
|
+
workspace = path.resolve(dockerArgs[++i]);
|
|
419
|
+
break;
|
|
420
|
+
case "--memory":
|
|
421
|
+
if (i + 1 >= dockerArgs.length) { console.error("Error: --memory requires a value (e.g. 2g)."); process.exit(1); }
|
|
422
|
+
memory = dockerArgs[++i];
|
|
423
|
+
break;
|
|
424
|
+
case "--cpus":
|
|
425
|
+
if (i + 1 >= dockerArgs.length) { console.error("Error: --cpus requires a value (e.g. 1.0)."); process.exit(1); }
|
|
426
|
+
cpus = dockerArgs[++i];
|
|
427
|
+
break;
|
|
428
|
+
case "--name":
|
|
429
|
+
if (i + 1 >= dockerArgs.length) { console.error("Error: --name requires a value."); process.exit(1); }
|
|
430
|
+
containerName = dockerArgs[++i];
|
|
431
|
+
break;
|
|
432
|
+
case "--no-rm":
|
|
433
|
+
autoRemove = false;
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
// First unrecognized non-flag arg is the prompt (for 'run' mode)
|
|
437
|
+
if (!interactive && !prompt && !arg.startsWith("-")) {
|
|
438
|
+
prompt = arg;
|
|
439
|
+
} else {
|
|
440
|
+
extraClaudeArgs.push(arg);
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
i++;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Validate
|
|
448
|
+
if (!interactive && !prompt) {
|
|
449
|
+
console.error(
|
|
450
|
+
'Error: No prompt provided.\n\n' +
|
|
451
|
+
' Usage: cloding docker run "your prompt here"\n'
|
|
452
|
+
);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!dockerImageExists()) {
|
|
457
|
+
console.error(
|
|
458
|
+
`Error: Docker image '${DOCKER_IMAGE}' not found.\n\n` +
|
|
459
|
+
" Build it first: cloding docker build\n"
|
|
460
|
+
);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
465
|
+
if (!apiKey) {
|
|
466
|
+
console.error(
|
|
467
|
+
"Error: OPENROUTER_API_KEY not set.\n\n" +
|
|
468
|
+
"Get your key at https://openrouter.ai/keys\n" +
|
|
469
|
+
"Then: export OPENROUTER_API_KEY=sk-or-v1-...\n"
|
|
470
|
+
);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Resolve model
|
|
475
|
+
const model = resolveModel(modelArg, models);
|
|
476
|
+
|
|
477
|
+
// Validate workspace exists
|
|
478
|
+
if (!fs.existsSync(workspace)) {
|
|
479
|
+
console.error(`Error: Workspace directory not found: ${workspace}`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Ensure network exists
|
|
484
|
+
ensureNetwork();
|
|
485
|
+
|
|
486
|
+
// Generate container name if not provided
|
|
487
|
+
if (!containerName) {
|
|
488
|
+
const suffix = Date.now().toString(36);
|
|
489
|
+
containerName = `cloding-${interactive ? "shell" : "run"}-${suffix}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Build docker command — uses spawn with argument array (safe, no shell injection)
|
|
493
|
+
const cmd = ["docker", "run"];
|
|
494
|
+
|
|
495
|
+
if (interactive) {
|
|
496
|
+
cmd.push("-it");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (autoRemove) {
|
|
500
|
+
cmd.push("--rm");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
cmd.push(
|
|
504
|
+
"--name", containerName,
|
|
505
|
+
"--network", DOCKER_NETWORK,
|
|
506
|
+
"--memory", memory,
|
|
507
|
+
"--cpus", cpus,
|
|
508
|
+
"-v", `${workspace}:/workspace`,
|
|
509
|
+
"-e", `ANTHROPIC_BASE_URL=https://openrouter.ai/api`,
|
|
510
|
+
"-e", `ANTHROPIC_AUTH_TOKEN=${apiKey}`,
|
|
511
|
+
"-e", `ANTHROPIC_API_KEY=`,
|
|
512
|
+
"-e", `ANTHROPIC_MODEL=${model.id}`,
|
|
513
|
+
"-e", `CLAUDECODE=`,
|
|
514
|
+
DOCKER_IMAGE
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Add claude args
|
|
518
|
+
if (!interactive && prompt) {
|
|
519
|
+
cmd.push("-p", prompt);
|
|
520
|
+
}
|
|
521
|
+
cmd.push(...extraClaudeArgs);
|
|
522
|
+
|
|
523
|
+
// Print banner
|
|
524
|
+
const costInfo =
|
|
525
|
+
model.in > 0 ? ` ($${model.in}/$${model.out} per Mtok)` : "";
|
|
526
|
+
console.log(
|
|
527
|
+
`\x1b[36m⚡ cloding docker\x1b[0m → ${model.name}${costInfo}`
|
|
528
|
+
);
|
|
529
|
+
console.log(` Container: ${containerName}`);
|
|
530
|
+
console.log(` Workspace: ${workspace} → /workspace`);
|
|
531
|
+
console.log(` Resources: ${memory} RAM, ${cpus} CPUs`);
|
|
532
|
+
|
|
533
|
+
if (model.in > 0 && models.opus) {
|
|
534
|
+
const savings = (models.opus.out / model.out).toFixed(0);
|
|
535
|
+
if (savings > 1) {
|
|
536
|
+
console.log(` \x1b[32m${savings}x cheaper than Opus\x1b[0m`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
console.log("");
|
|
540
|
+
|
|
541
|
+
// Spawn docker — uses argument array (no shell interpretation)
|
|
542
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
543
|
+
stdio: "inherit",
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
547
|
+
child.on("error", (err) => {
|
|
548
|
+
console.error(`Error launching Docker: ${err.message}`);
|
|
549
|
+
console.error("Make sure Docker is installed and running.");
|
|
550
|
+
process.exit(1);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function dockerStatus() {
|
|
555
|
+
console.log("\x1b[36m⚡ cloding\x1b[0m Running containers:\n");
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
// Safe: uses spawnSync with argument array, no shell injection possible
|
|
559
|
+
const result = require("child_process").spawnSync(
|
|
560
|
+
"docker",
|
|
561
|
+
["ps", "--filter", "name=cloding", "--format", "table {{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}"],
|
|
562
|
+
{ encoding: "utf8" }
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (result.stdout && result.stdout.trim()) {
|
|
566
|
+
console.log(result.stdout);
|
|
567
|
+
} else {
|
|
568
|
+
console.log(" No running cloding containers.\n");
|
|
569
|
+
console.log(" Start one with:");
|
|
570
|
+
console.log(' cloding docker run "your prompt"');
|
|
571
|
+
console.log(" cloding docker shell\n");
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
console.error(
|
|
575
|
+
"Error: Could not list containers. Is Docker running?"
|
|
576
|
+
);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function dockerStop() {
|
|
582
|
+
console.log("\x1b[36m⚡ cloding\x1b[0m Stopping all cloding containers...\n");
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
// Safe: uses spawnSync with argument array
|
|
586
|
+
const result = require("child_process").spawnSync(
|
|
587
|
+
"docker",
|
|
588
|
+
["ps", "-q", "--filter", "name=cloding"],
|
|
589
|
+
{ encoding: "utf8" }
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const ids = (result.stdout || "")
|
|
593
|
+
.trim()
|
|
594
|
+
.split("\n")
|
|
595
|
+
.filter((id) => id);
|
|
596
|
+
|
|
597
|
+
if (ids.length === 0) {
|
|
598
|
+
console.log(" No running cloding containers to stop.\n");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const id of ids) {
|
|
603
|
+
try {
|
|
604
|
+
const inspect = require("child_process").spawnSync(
|
|
605
|
+
"docker", ["inspect", "--format", "{{.Name}}", id],
|
|
606
|
+
{ encoding: "utf8" }
|
|
607
|
+
);
|
|
608
|
+
const name = (inspect.stdout || id).trim().replace(/^\//, "");
|
|
609
|
+
|
|
610
|
+
require("child_process").spawnSync(
|
|
611
|
+
"docker", ["stop", "-t", "5", id],
|
|
612
|
+
{ stdio: "ignore" }
|
|
613
|
+
);
|
|
614
|
+
console.log(` \x1b[32m✓\x1b[0m Stopped: ${name}`);
|
|
615
|
+
} catch {
|
|
616
|
+
console.log(` \x1b[31m✗\x1b[0m Failed to stop: ${id}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
console.log(`\n Stopped ${ids.length} container(s).\n`);
|
|
620
|
+
} catch {
|
|
621
|
+
console.error("Error: Could not stop containers. Is Docker running?");
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function dockerClean() {
|
|
627
|
+
console.log(
|
|
628
|
+
"\x1b[36m⚡ cloding\x1b[0m Cleaning up stopped cloding containers...\n"
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
// Safe: uses spawnSync with argument array
|
|
633
|
+
const result = require("child_process").spawnSync(
|
|
634
|
+
"docker",
|
|
635
|
+
["ps", "-aq", "--filter", "name=cloding", "--filter", "status=exited"],
|
|
636
|
+
{ encoding: "utf8" }
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
const ids = (result.stdout || "")
|
|
640
|
+
.trim()
|
|
641
|
+
.split("\n")
|
|
642
|
+
.filter((id) => id);
|
|
643
|
+
|
|
644
|
+
if (ids.length === 0) {
|
|
645
|
+
console.log(" No stopped cloding containers to clean.\n");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
for (const id of ids) {
|
|
650
|
+
try {
|
|
651
|
+
const inspect = require("child_process").spawnSync(
|
|
652
|
+
"docker", ["inspect", "--format", "{{.Name}}", id],
|
|
653
|
+
{ encoding: "utf8" }
|
|
654
|
+
);
|
|
655
|
+
const name = (inspect.stdout || id).trim().replace(/^\//, "");
|
|
656
|
+
|
|
657
|
+
require("child_process").spawnSync(
|
|
658
|
+
"docker", ["rm", id],
|
|
659
|
+
{ stdio: "ignore" }
|
|
660
|
+
);
|
|
661
|
+
console.log(` \x1b[32m✓\x1b[0m Removed: ${name}`);
|
|
662
|
+
} catch {
|
|
663
|
+
console.log(` \x1b[31m✗\x1b[0m Failed to remove: ${id}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
console.log(`\n Cleaned ${ids.length} container(s).\n`);
|
|
667
|
+
} catch {
|
|
668
|
+
console.error(
|
|
669
|
+
"Error: Could not clean containers. Is Docker running?"
|
|
670
|
+
);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ──────────────────────────────────────────────
|
|
676
|
+
// Docker dispatcher
|
|
677
|
+
// ──────────────────────────────────────────────
|
|
678
|
+
function handleDocker(args) {
|
|
679
|
+
if (!dockerAvailable()) {
|
|
680
|
+
console.error(
|
|
681
|
+
"Error: Docker not found.\n\n" +
|
|
682
|
+
"Install Docker Desktop:\n" +
|
|
683
|
+
" https://docs.docker.com/get-docker/\n"
|
|
684
|
+
);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const models = loadModels();
|
|
689
|
+
|
|
690
|
+
switch (args.dockerSubcommand) {
|
|
691
|
+
case "build":
|
|
692
|
+
dockerBuild();
|
|
693
|
+
break;
|
|
694
|
+
case "run":
|
|
695
|
+
dockerRun(args.dockerArgs, models, false);
|
|
696
|
+
break;
|
|
697
|
+
case "shell":
|
|
698
|
+
dockerRun(args.dockerArgs, models, true);
|
|
699
|
+
break;
|
|
700
|
+
case "status":
|
|
701
|
+
case "ps":
|
|
702
|
+
dockerStatus();
|
|
703
|
+
break;
|
|
704
|
+
case "stop":
|
|
705
|
+
dockerStop();
|
|
706
|
+
break;
|
|
707
|
+
case "clean":
|
|
708
|
+
case "cleanup":
|
|
709
|
+
case "prune":
|
|
710
|
+
dockerClean();
|
|
711
|
+
break;
|
|
712
|
+
case "help":
|
|
713
|
+
case "--help":
|
|
714
|
+
case "-h":
|
|
715
|
+
printDockerHelp();
|
|
716
|
+
break;
|
|
717
|
+
default:
|
|
718
|
+
console.error(
|
|
719
|
+
`Unknown docker command: ${args.dockerSubcommand}\n`
|
|
720
|
+
);
|
|
721
|
+
printDockerHelp();
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ──────────────────────────────────────────────
|
|
727
|
+
// Main
|
|
728
|
+
// ──────────────────────────────────────────────
|
|
729
|
+
function main() {
|
|
730
|
+
// Load .env
|
|
731
|
+
loadEnvFile();
|
|
732
|
+
|
|
733
|
+
// Parse args (skip node and script path)
|
|
734
|
+
const args = parseArgs(process.argv.slice(2));
|
|
735
|
+
|
|
736
|
+
if (args.version) {
|
|
737
|
+
printVersion();
|
|
738
|
+
process.exit(0);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (args.help) {
|
|
742
|
+
printHelp();
|
|
743
|
+
process.exit(0);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const models = loadModels();
|
|
747
|
+
|
|
748
|
+
if (args.listModels) {
|
|
749
|
+
printModels(models);
|
|
750
|
+
process.exit(0);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── Docker mode ──
|
|
754
|
+
if (args.docker) {
|
|
755
|
+
handleDocker(args);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Validate API key
|
|
760
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
761
|
+
if (!apiKey) {
|
|
762
|
+
console.error(
|
|
763
|
+
"Error: OPENROUTER_API_KEY not set.\n\n" +
|
|
764
|
+
"Get your key at https://openrouter.ai/keys\n" +
|
|
765
|
+
"Then either:\n" +
|
|
766
|
+
" 1. Create a .env file: OPENROUTER_API_KEY=sk-or-v1-...\n" +
|
|
767
|
+
" 2. Set it: export OPENROUTER_API_KEY=sk-or-v1-...\n"
|
|
768
|
+
);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ── Pipeline mode ──
|
|
773
|
+
if (args.pipeline) {
|
|
774
|
+
const pipelineDir = path.join(__dirname, "..", "pipeline");
|
|
775
|
+
if (!fs.existsSync(pipelineDir)) {
|
|
776
|
+
console.error(
|
|
777
|
+
"Error: Pipeline not found. Install the full cloding package with Python support."
|
|
778
|
+
);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const pythonArgs = ["-m", "osq", ...args.pipelineArgs];
|
|
783
|
+
console.log(`Running pipeline: python ${pythonArgs.join(" ")}`);
|
|
784
|
+
|
|
785
|
+
const child = spawn("python", pythonArgs, {
|
|
786
|
+
cwd: pipelineDir,
|
|
787
|
+
stdio: "inherit",
|
|
788
|
+
env: { ...process.env },
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
792
|
+
child.on("error", (err) => {
|
|
793
|
+
console.error(`Error launching pipeline: ${err.message}`);
|
|
794
|
+
console.error("Make sure Python 3.11+ is installed.");
|
|
795
|
+
process.exit(1);
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ── Simple mode: launch claude with OpenRouter ──
|
|
801
|
+
const model = resolveModel(args.model, models);
|
|
802
|
+
|
|
803
|
+
// Build env for claude
|
|
804
|
+
const claudeEnv = { ...process.env };
|
|
805
|
+
claudeEnv.ANTHROPIC_BASE_URL = "https://openrouter.ai/api";
|
|
806
|
+
claudeEnv.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
807
|
+
claudeEnv.ANTHROPIC_API_KEY = "";
|
|
808
|
+
claudeEnv.ANTHROPIC_MODEL = model.id;
|
|
809
|
+
|
|
810
|
+
// Don't inherit CLAUDECODE — prevents "cannot launch inside another session" error
|
|
811
|
+
delete claudeEnv.CLAUDECODE;
|
|
812
|
+
|
|
813
|
+
// Build claude args
|
|
814
|
+
const claudeArgs = [...args.claudeArgs];
|
|
815
|
+
if (args.prompt) {
|
|
816
|
+
claudeArgs.push("-p", args.prompt);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Print banner
|
|
820
|
+
const shortcut = args.model || process.env.CLODING_DEFAULT_MODEL || "qwen";
|
|
821
|
+
const costInfo =
|
|
822
|
+
model.in > 0
|
|
823
|
+
? ` ($${model.in}/$${model.out} per Mtok)`
|
|
824
|
+
: "";
|
|
825
|
+
console.log(`\x1b[36m⚡ cloding\x1b[0m → ${model.name}${costInfo}`);
|
|
826
|
+
|
|
827
|
+
if (model.in > 0 && models.opus) {
|
|
828
|
+
const savings = (models.opus.out / model.out).toFixed(0);
|
|
829
|
+
if (savings > 1) {
|
|
830
|
+
console.log(`\x1b[32m ${savings}x cheaper than Opus\x1b[0m`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
console.log("");
|
|
834
|
+
|
|
835
|
+
// Launch claude
|
|
836
|
+
// On Windows, npm globals are .cmd shims that need shell resolution.
|
|
837
|
+
// We use spawn with shell:true but pass empty args array to avoid the
|
|
838
|
+
// DEP0190 deprecation — instead the args are baked into the command string.
|
|
839
|
+
const isWin = process.platform === "win32";
|
|
840
|
+
let child;
|
|
841
|
+
if (isWin) {
|
|
842
|
+
// Build full command string for shell execution
|
|
843
|
+
const parts = ["claude", ...claudeArgs.map((a) => `"${a}"`)];
|
|
844
|
+
child = spawn(parts.join(" "), {
|
|
845
|
+
stdio: "inherit",
|
|
846
|
+
env: claudeEnv,
|
|
847
|
+
shell: true,
|
|
848
|
+
});
|
|
849
|
+
} else {
|
|
850
|
+
child = spawn("claude", claudeArgs, {
|
|
851
|
+
stdio: "inherit",
|
|
852
|
+
env: claudeEnv,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
857
|
+
child.on("error", (err) => {
|
|
858
|
+
if (err.code === "ENOENT") {
|
|
859
|
+
console.error(
|
|
860
|
+
"Error: 'claude' command not found.\n\n" +
|
|
861
|
+
"Install Claude Code first:\n" +
|
|
862
|
+
" npm install -g @anthropic-ai/claude-code\n"
|
|
863
|
+
);
|
|
864
|
+
} else {
|
|
865
|
+
console.error(`Error launching claude: ${err.message}`);
|
|
866
|
+
}
|
|
867
|
+
process.exit(1);
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
main();
|
package/models.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"qwen": {
|
|
3
|
+
"id": "qwen/qwen3-coder-next",
|
|
4
|
+
"name": "Qwen 3 Coder",
|
|
5
|
+
"in": 0.07,
|
|
6
|
+
"out": 0.30,
|
|
7
|
+
"description": "Best value. Handles all Claude Code tools perfectly."
|
|
8
|
+
},
|
|
9
|
+
"haiku": {
|
|
10
|
+
"id": "anthropic/claude-haiku-4.5",
|
|
11
|
+
"name": "Claude Haiku 4.5",
|
|
12
|
+
"in": 0.80,
|
|
13
|
+
"out": 4.00,
|
|
14
|
+
"description": "Fast and capable. Good for exploration tasks."
|
|
15
|
+
},
|
|
16
|
+
"sonnet": {
|
|
17
|
+
"id": "anthropic/claude-sonnet-4",
|
|
18
|
+
"name": "Claude Sonnet 4",
|
|
19
|
+
"in": 3.00,
|
|
20
|
+
"out": 15.00,
|
|
21
|
+
"description": "Strong all-rounder. Good for complex coding."
|
|
22
|
+
},
|
|
23
|
+
"opus": {
|
|
24
|
+
"id": "anthropic/claude-opus-4.6",
|
|
25
|
+
"name": "Claude Opus 4.6",
|
|
26
|
+
"in": 15.00,
|
|
27
|
+
"out": 75.00,
|
|
28
|
+
"description": "Most capable. Use for architecture and planning."
|
|
29
|
+
},
|
|
30
|
+
"deepseek": {
|
|
31
|
+
"id": "deepseek/deepseek-coder-v3",
|
|
32
|
+
"name": "DeepSeek Coder V3",
|
|
33
|
+
"in": 0.14,
|
|
34
|
+
"out": 0.28,
|
|
35
|
+
"description": "Ultra-cheap alternative. Good for simple tasks."
|
|
36
|
+
},
|
|
37
|
+
"gemini": {
|
|
38
|
+
"id": "google/gemini-2.5-pro",
|
|
39
|
+
"name": "Gemini 2.5 Pro",
|
|
40
|
+
"in": 1.25,
|
|
41
|
+
"out": 10.00,
|
|
42
|
+
"description": "Google's flagship. Large context window."
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloding",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code with any model via OpenRouter. Use Qwen, Haiku, Sonnet, or Opus at a fraction of the cost.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cloding": "./bin/cloding.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"openrouter",
|
|
12
|
+
"qwen",
|
|
13
|
+
"ai",
|
|
14
|
+
"coding",
|
|
15
|
+
"llm",
|
|
16
|
+
"cheap",
|
|
17
|
+
"cost-effective"
|
|
18
|
+
],
|
|
19
|
+
"author": "Carlos",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/claudlos/cloding"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/claudlos/cloding#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/claudlos/cloding/issues"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"models.json",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
]
|
|
38
|
+
}
|