cc-hub-cli 1.0.3 → 1.0.6

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.
Files changed (3) hide show
  1. package/README.md +65 -1
  2. package/dist/index.js +691 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,9 +11,12 @@ npm install -g cc-hub-cli
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- # Add a profile
14
+ # Add a profile with single model
15
15
  cc-hub profile add flow -m anthropic.claude-4-6-sonnet -t eyJ... -u https://example.com/api
16
16
 
17
+ # Add a profile with multiple models
18
+ cc-hub profile add multi -m kimi-k2.5 -m claude-sonnet-4-6 -m gpt-4 -t eyJ... -u https://api.example.com
19
+
17
20
  # Set as default
18
21
  cc-hub profile default flow
19
22
 
@@ -32,7 +35,10 @@ Manage multiple Claude API configurations (model, token, URL).
32
35
 
33
36
  ```bash
34
37
  cc-hub profile add <name> -m <model> -t <token> -u <url> # Add or update
38
+ cc-hub profile add <name> -m <m1> -m <m2> -t <token> # Add with multiple models
35
39
  cc-hub profile update <name> -m <model> # Update fields
40
+ cc-hub profile update <name> -m <m1> -m <m2> # Update with multiple models
41
+ cc-hub profile remove-model <name> -m <model> # Remove specific models
36
42
  cc-hub profile list # List all (tokens masked)
37
43
  cc-hub profile view <name> # View details (token visible)
38
44
  cc-hub profile view <name> -j # View as JSON
@@ -40,6 +46,24 @@ cc-hub profile remove <name> # Remove
40
46
  cc-hub profile default <name> # Set default
41
47
  ```
42
48
 
49
+ **Multi-Model Profiles:**
50
+
51
+ You can specify multiple models per profile using the `-m` flag multiple times:
52
+
53
+ ```bash
54
+ # Add a profile with multiple models
55
+ cc-hub profile add myprofile -m kimi-k2.5 -m claude-sonnet-4-6 -t <token> -u <url>
56
+
57
+ # Remove specific models
58
+ cc-hub profile remove-model myprofile -m kimi-k2.5
59
+ ```
60
+
61
+ When you launch Claude Code with `cc-hub run`, the models are automatically populated into Claude Code's `/model` picker via the `availableModels` setting in `~/.claude/settings.json`. The first model is used as the default.
62
+
63
+ **Non-Anthropic Models:**
64
+
65
+ If your profile includes non-Anthropic models (e.g., `kimi-k2.5`, `gpt-4`), the first such model is also exposed via the `ANTHROPIC_CUSTOM_MODEL_OPTION` environment variable, which adds it as a named entry in the `/model` picker.
66
+
43
67
  ### Run / Use
44
68
 
45
69
  Launch Claude Code with a profile's credentials injected as environment variables.
@@ -139,6 +163,8 @@ cc-hub reads from these paths (overridable via environment variables):
139
163
 
140
164
  ### Profile storage format
141
165
 
166
+ Single-model profile:
167
+
142
168
  ```json
143
169
  {
144
170
  "profiles": {
@@ -152,6 +178,44 @@ cc-hub reads from these paths (overridable via environment variables):
152
178
  }
153
179
  ```
154
180
 
181
+ Multi-model profile:
182
+
183
+ ```json
184
+ {
185
+ "profiles": {
186
+ "multi": {
187
+ "model": "kimi-k2.5",
188
+ "models": ["kimi-k2.5", "claude-sonnet-4-6", "gpt-4"],
189
+ "token": "eyJ...",
190
+ "url": "https://api.example.com"
191
+ }
192
+ },
193
+ "default": "multi"
194
+ }
195
+ ```
196
+
197
+ When launching with a multi-model profile, cc-hub automatically configures Claude Code's `availableModels` setting, populating the `/model` picker with all specified models.
198
+
199
+ ## Claude Code Skill
200
+
201
+ cc-hub includes a Claude Code skill for natural language profile management. The skill is located at `skills/cc-hub/SKILL.md`.
202
+
203
+ **Capabilities:**
204
+ - Add/remove models from profiles using natural language
205
+ - List models in a profile
206
+ - Set the default model for a profile
207
+
208
+ **Examples:**
209
+
210
+ ```
211
+ Add kimi-k2.5 and gpt-4 to my hy profile
212
+ Remove claude-sonnet from the flow profile
213
+ What models are in my hy profile?
214
+ Set the default model for hy to gpt-4
215
+ ```
216
+
217
+ The skill directly reads and writes `~/.claude/profiles.json` to make changes.
218
+
155
219
  ## License
156
220
 
157
221
  MIT
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command5 } from "commander";
4
+ import { Command as Command6 } from "commander";
5
5
 
6
6
  // src/profiles.ts
7
- import { Command } from "commander";
8
- import { spawnSync } from "child_process";
7
+ import { Command as Command2 } from "commander";
8
+ import { spawnSync, spawn } from "child_process";
9
9
 
10
10
  // src/config.ts
11
11
  import fs from "fs";
@@ -14,6 +14,7 @@ import os from "os";
14
14
  var CLAUDE_DIR = process.env.CLAUDE_DIR || path.join(os.homedir(), ".claude");
15
15
  var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path.join(CLAUDE_DIR, "profiles.json");
16
16
  var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path.join(CLAUDE_DIR, "settings.json");
17
+ var CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
17
18
  var PROJECTS_DIR = path.join(CLAUDE_DIR, "projects");
18
19
  var SESSIONS_DIR = path.join(CLAUDE_DIR, "sessions");
19
20
  function ensureFile(filePath, defaultContent) {
@@ -34,6 +35,372 @@ function ensureProfilesFile() {
34
35
  function ensureSettingsFile() {
35
36
  ensureFile(SETTINGS_FILE, "{}\n");
36
37
  }
38
+ function fixJsonFile(filePath, fallback = {}) {
39
+ if (!fs.existsSync(filePath)) return;
40
+ const backupPath = path.join(CLAUDE_DIR, path.basename(filePath) + ".backup");
41
+ const raw = fs.readFileSync(filePath, "utf-8");
42
+ try {
43
+ JSON.parse(raw);
44
+ fs.copyFileSync(filePath, backupPath);
45
+ return;
46
+ } catch {
47
+ }
48
+ let text = raw.trim();
49
+ if (text.charCodeAt(0) === 65279) {
50
+ text = text.slice(1).trim();
51
+ }
52
+ text = text.replace(/,\s*$/, "");
53
+ text = text.replace(/,\s*([}\]])/g, "$1");
54
+ const lastBrace = Math.max(text.lastIndexOf("}"), text.lastIndexOf("]"));
55
+ if (lastBrace !== -1 && lastBrace < text.length - 1) {
56
+ text = text.slice(0, lastBrace + 1);
57
+ }
58
+ let openCurly = 0, openSquare = 0;
59
+ for (const ch of text) {
60
+ if (ch === "{") openCurly++;
61
+ else if (ch === "}") openCurly--;
62
+ else if (ch === "[") openSquare++;
63
+ else if (ch === "]") openSquare--;
64
+ }
65
+ if (openSquare > 0) text += "]".repeat(openSquare);
66
+ if (openCurly > 0) text += "}".repeat(openCurly);
67
+ try {
68
+ JSON.parse(text);
69
+ fs.writeFileSync(filePath, text + "\n", "utf-8");
70
+ console.error(`Fixed invalid JSON in ${path.basename(filePath)}.`);
71
+ } catch {
72
+ if (fs.existsSync(backupPath)) {
73
+ fs.copyFileSync(backupPath, filePath);
74
+ console.error(`Restored ${path.basename(filePath)} from backup.`);
75
+ } else {
76
+ writeJson(filePath, fallback);
77
+ console.error(`Could not fix ${path.basename(filePath)}, no backup found, reset to default.`);
78
+ }
79
+ }
80
+ }
81
+
82
+ // src/provider.ts
83
+ import http from "http";
84
+ import { Command } from "commander";
85
+ function sanitizeToolId(id) {
86
+ let sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
87
+ if (!/^[a-zA-Z]/.test(sanitized)) {
88
+ sanitized = "tc_" + sanitized;
89
+ }
90
+ return sanitized;
91
+ }
92
+ function transformAnthropicToOpenAI(body) {
93
+ const messages = [];
94
+ if (body.system) {
95
+ if (typeof body.system === "string") {
96
+ messages.push({ role: "system", content: body.system });
97
+ } else if (Array.isArray(body.system)) {
98
+ const text = body.system.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
99
+ if (text) messages.push({ role: "system", content: text });
100
+ }
101
+ }
102
+ for (const msg of body.messages ?? []) {
103
+ if (typeof msg.content === "string") {
104
+ messages.push({ role: msg.role, content: msg.content });
105
+ continue;
106
+ }
107
+ if (!Array.isArray(msg.content)) {
108
+ messages.push({ role: msg.role, content: JSON.stringify(msg.content) });
109
+ continue;
110
+ }
111
+ if (msg.role === "user") {
112
+ const toolResults = msg.content.filter(
113
+ (b) => b.type === "tool_result" && b.tool_use_id
114
+ );
115
+ for (const tr of toolResults) {
116
+ messages.push({
117
+ role: "tool",
118
+ tool_call_id: sanitizeToolId(tr.tool_use_id),
119
+ content: typeof tr.content === "string" ? tr.content : Array.isArray(tr.content) ? tr.content.filter((b) => b.type === "text").map((b) => b.text).join("") : JSON.stringify(tr.content)
120
+ });
121
+ }
122
+ const contentParts = msg.content.filter(
123
+ (b) => b.type === "text" && b.text || b.type === "image" && b.source
124
+ );
125
+ if (contentParts.length > 0) {
126
+ const converted = contentParts.map((part) => {
127
+ if (part.type === "image") {
128
+ const url = part.source?.type === "base64" ? `data:${part.source.media_type};base64,${part.source.data}` : part.source?.url ?? "";
129
+ return { type: "image_url", image_url: { url } };
130
+ }
131
+ return { type: "text", text: part.text };
132
+ });
133
+ if (converted.every((p) => p.type === "text")) {
134
+ messages.push({
135
+ role: "user",
136
+ content: converted.map((p) => p.text).join("")
137
+ });
138
+ } else {
139
+ messages.push({ role: "user", content: converted });
140
+ }
141
+ }
142
+ } else if (msg.role === "assistant") {
143
+ const assistantMsg = { role: "assistant", content: null };
144
+ const textParts = msg.content.filter(
145
+ (b) => b.type === "text" && b.text
146
+ );
147
+ if (textParts.length > 0) {
148
+ assistantMsg.content = textParts.map((b) => b.text).join("\n");
149
+ }
150
+ const toolUseParts = msg.content.filter(
151
+ (b) => b.type === "tool_use" && b.id
152
+ );
153
+ if (toolUseParts.length > 0) {
154
+ assistantMsg.tool_calls = toolUseParts.map((b) => ({
155
+ id: sanitizeToolId(b.id),
156
+ type: "function",
157
+ function: {
158
+ name: b.name,
159
+ arguments: JSON.stringify(b.input ?? {})
160
+ }
161
+ }));
162
+ }
163
+ messages.push(assistantMsg);
164
+ }
165
+ }
166
+ const result = {
167
+ model: body.model,
168
+ messages,
169
+ stream: body.stream ?? false
170
+ };
171
+ if (body.max_tokens != null) result.max_tokens = body.max_tokens;
172
+ if (body.temperature != null) result.temperature = body.temperature;
173
+ if (body.tools?.length) {
174
+ result.tools = body.tools.map((t) => ({
175
+ type: "function",
176
+ function: {
177
+ name: t.name,
178
+ description: t.description ?? "",
179
+ parameters: t.input_schema
180
+ }
181
+ }));
182
+ if (body.tool_choice) {
183
+ const tc = body.tool_choice;
184
+ if (tc.type === "auto" || tc.type === "none" || tc.type === "required") {
185
+ result.tool_choice = tc.type;
186
+ } else if (tc.type === "tool") {
187
+ result.tool_choice = {
188
+ type: "function",
189
+ function: { name: tc.name }
190
+ };
191
+ }
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+ function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
197
+ const choice = openaiResponse.choices?.[0];
198
+ if (!choice) throw new Error("No choices in OpenAI response");
199
+ const content = [];
200
+ if (choice.message?.content) {
201
+ content.push({ type: "text", text: choice.message.content });
202
+ }
203
+ if (choice.message?.tool_calls?.length) {
204
+ for (const tc of choice.message.tool_calls) {
205
+ let input = {};
206
+ try {
207
+ input = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
208
+ } catch {
209
+ input = { text: tc.function.arguments ?? "" };
210
+ }
211
+ content.push({
212
+ type: "tool_use",
213
+ id: tc.id,
214
+ name: tc.function.name,
215
+ input
216
+ });
217
+ }
218
+ }
219
+ const finishMap = {
220
+ stop: "end_turn",
221
+ length: "max_tokens",
222
+ tool_calls: "tool_use",
223
+ content_filter: "stop_sequence"
224
+ };
225
+ return {
226
+ id: openaiResponse.id ?? `msg_${Date.now()}`,
227
+ type: "message",
228
+ role: "assistant",
229
+ model: openaiResponse.model ?? originalModel,
230
+ content,
231
+ stop_reason: finishMap[choice.finish_reason] ?? "end_turn",
232
+ stop_sequence: null,
233
+ usage: {
234
+ input_tokens: (openaiResponse.usage?.prompt_tokens ?? 0) - (openaiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0),
235
+ output_tokens: openaiResponse.usage?.completion_tokens ?? 0,
236
+ cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0
237
+ }
238
+ };
239
+ }
240
+ function* synthesizeAnthropicSSE(anthropicResponse) {
241
+ const sse = (event, data) => `event: ${event}
242
+ data: ${JSON.stringify(data)}
243
+
244
+ `;
245
+ const usage = anthropicResponse.usage ?? {};
246
+ yield sse("message_start", {
247
+ type: "message_start",
248
+ message: {
249
+ id: anthropicResponse.id,
250
+ type: "message",
251
+ role: "assistant",
252
+ content: [],
253
+ model: anthropicResponse.model,
254
+ stop_reason: null,
255
+ stop_sequence: null,
256
+ usage: {
257
+ input_tokens: usage.input_tokens ?? 0,
258
+ output_tokens: 0,
259
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
260
+ }
261
+ }
262
+ });
263
+ for (let i = 0; i < (anthropicResponse.content ?? []).length; i++) {
264
+ const block = anthropicResponse.content[i];
265
+ yield sse("content_block_start", {
266
+ type: "content_block_start",
267
+ index: i,
268
+ content_block: block.type === "tool_use" ? { type: "tool_use", id: block.id, name: block.name, input: {} } : { type: "text", text: "" }
269
+ });
270
+ if (block.type === "text" && block.text) {
271
+ yield sse("content_block_delta", {
272
+ type: "content_block_delta",
273
+ index: i,
274
+ delta: { type: "text_delta", text: block.text }
275
+ });
276
+ } else if (block.type === "tool_use" && block.input) {
277
+ yield sse("content_block_delta", {
278
+ type: "content_block_delta",
279
+ index: i,
280
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(block.input) }
281
+ });
282
+ }
283
+ yield sse("content_block_stop", { type: "content_block_stop", index: i });
284
+ }
285
+ yield sse("message_delta", {
286
+ type: "message_delta",
287
+ delta: {
288
+ stop_reason: anthropicResponse.stop_reason ?? "end_turn",
289
+ stop_sequence: anthropicResponse.stop_sequence ?? null
290
+ },
291
+ usage: { output_tokens: usage.output_tokens ?? 0 }
292
+ });
293
+ yield sse("message_stop", { type: "message_stop" });
294
+ }
295
+ async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
296
+ const base = targetUrl.replace(/\/+$/, "");
297
+ const server = http.createServer(async (req, res) => {
298
+ try {
299
+ if (req.method === "GET" && req.url === "/v1/models") {
300
+ res.writeHead(200, { "Content-Type": "application/json" });
301
+ const modelList = models.length > 0 ? models : [model];
302
+ const response = {
303
+ object: "list",
304
+ data: modelList.map((m) => ({ id: m, object: "model" }))
305
+ };
306
+ res.end(JSON.stringify(response));
307
+ return;
308
+ }
309
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
310
+ const body = await readBody(req);
311
+ let parsed;
312
+ try {
313
+ parsed = JSON.parse(body);
314
+ } catch {
315
+ res.writeHead(400, { "Content-Type": "application/json" });
316
+ res.end(JSON.stringify({ error: { type: "invalid_request_error", message: "invalid JSON" } }));
317
+ return;
318
+ }
319
+ const isStream = !!parsed.stream;
320
+ const openaiBody = transformAnthropicToOpenAI({ ...parsed, stream: false });
321
+ const upstream = await fetch(`${base}/v1/chat/completions`, {
322
+ method: "POST",
323
+ headers: {
324
+ "Content-Type": "application/json",
325
+ "Authorization": `Bearer ${apiKey}`
326
+ },
327
+ body: JSON.stringify(openaiBody)
328
+ });
329
+ if (!upstream.ok) {
330
+ const errText = await upstream.text();
331
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
332
+ res.end(errText);
333
+ return;
334
+ }
335
+ const data = await upstream.json();
336
+ const anthropicResponse = transformOpenAIResponseToAnthropic(data, parsed.model ?? model);
337
+ if (isStream) {
338
+ res.writeHead(200, {
339
+ "Content-Type": "text/event-stream",
340
+ "Cache-Control": "no-cache",
341
+ "Connection": "keep-alive"
342
+ });
343
+ for (const chunk of synthesizeAnthropicSSE(anthropicResponse)) {
344
+ res.write(chunk);
345
+ }
346
+ res.end();
347
+ } else {
348
+ res.writeHead(200, { "Content-Type": "application/json" });
349
+ res.end(JSON.stringify(anthropicResponse));
350
+ }
351
+ return;
352
+ }
353
+ res.writeHead(404, { "Content-Type": "application/json" });
354
+ res.end(JSON.stringify({ error: { type: "not_found", message: "endpoint not found" } }));
355
+ } catch (err) {
356
+ if (!res.headersSent) {
357
+ res.writeHead(500, { "Content-Type": "application/json" });
358
+ res.end(JSON.stringify({ error: { type: "internal_error", message: String(err) } }));
359
+ }
360
+ }
361
+ });
362
+ return new Promise((resolve, reject) => {
363
+ server.listen(0, "127.0.0.1", () => {
364
+ const addr = server.address();
365
+ const baseUrl = `http://127.0.0.1:${addr.port}`;
366
+ resolve({
367
+ baseUrl,
368
+ stop: () => server.close()
369
+ });
370
+ });
371
+ server.on("error", reject);
372
+ });
373
+ }
374
+ function readBody(req) {
375
+ return new Promise((resolve, reject) => {
376
+ const chunks = [];
377
+ req.on("data", (c) => chunks.push(c));
378
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
379
+ req.on("error", reject);
380
+ });
381
+ }
382
+ var PROVIDERS = [
383
+ {
384
+ name: "anthropic",
385
+ description: "Default \u2014 sends Anthropic-format requests directly to the configured URL"
386
+ },
387
+ {
388
+ name: "openai",
389
+ description: "Embedded proxy \u2014 translates Anthropic requests to OpenAI Chat Completions format"
390
+ }
391
+ ];
392
+ function providerCommand() {
393
+ const cmd = new Command("provider").description("Manage provider types");
394
+ cmd.command("list").description("List available provider types").action(() => {
395
+ const fmt = (name, desc) => `${name.padEnd(12)} ${desc}`;
396
+ console.log(fmt("NAME", "DESCRIPTION"));
397
+ console.log(fmt("----", "-----------"));
398
+ for (const p of PROVIDERS) {
399
+ console.log(fmt(p.name, p.description));
400
+ }
401
+ });
402
+ return cmd;
403
+ }
37
404
 
38
405
  // src/profiles.ts
39
406
  function maskToken(token) {
@@ -41,20 +408,94 @@ function maskToken(token) {
41
408
  if (token.length <= 12) return token;
42
409
  return token.slice(0, 8) + "..." + token.slice(-4);
43
410
  }
411
+ function formatModels(p) {
412
+ if (p.models && p.models.length > 0) {
413
+ const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel(m));
414
+ const parts = [];
415
+ p.models.forEach((m, i) => {
416
+ if (!isAnthropicModel(m)) {
417
+ const aliasIndex = nonAnthropicModels.indexOf(m);
418
+ if (aliasIndex === 0) parts.push(`${m} (opus)`);
419
+ else if (aliasIndex === 1) parts.push(`${m} (sonnet)`);
420
+ else if (aliasIndex === 2) parts.push(`${m} (haiku)`);
421
+ else parts.push(m);
422
+ } else {
423
+ parts.push(m);
424
+ }
425
+ });
426
+ const joined = parts.join(", ");
427
+ if (joined.length > 28) {
428
+ return parts[0] + ", +" + (parts.length - 1) + " more";
429
+ }
430
+ return joined;
431
+ }
432
+ return p.model || "(unset)";
433
+ }
434
+ function isAnthropicModel(model) {
435
+ const anthropicAliases = ["opus", "sonnet", "haiku", "best", "default", "opusplan", "opus[1m]", "sonnet[1m]"];
436
+ const lower = model.toLowerCase();
437
+ if (anthropicAliases.includes(lower)) return true;
438
+ if (lower.startsWith("claude-")) return true;
439
+ return false;
440
+ }
441
+ function updateSettingsForProfile(p) {
442
+ ensureSettingsFile();
443
+ const settings = readJson(SETTINGS_FILE);
444
+ const models = p.models || (p.model ? [p.model] : []);
445
+ const nonAnthropicModels = models.filter((m) => !isAnthropicModel(m));
446
+ if (models.length > 0) {
447
+ settings.model = models[0];
448
+ if (nonAnthropicModels.length > 0) {
449
+ const aliases = [];
450
+ if (nonAnthropicModels[0]) aliases.push("opus");
451
+ if (nonAnthropicModels[1]) aliases.push("sonnet");
452
+ if (nonAnthropicModels[2]) aliases.push("haiku");
453
+ settings.availableModels = aliases;
454
+ } else {
455
+ settings.availableModels = models;
456
+ }
457
+ } else {
458
+ delete settings.model;
459
+ delete settings.availableModels;
460
+ }
461
+ const envVarsToClean = [
462
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
463
+ "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
464
+ "ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION",
465
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
466
+ "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
467
+ "ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION",
468
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
469
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
470
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
471
+ "ANTHROPIC_CUSTOM_MODEL_OPTION"
472
+ ];
473
+ if (settings.env) {
474
+ for (const key of envVarsToClean) {
475
+ delete settings.env[key];
476
+ }
477
+ }
478
+ writeJson(SETTINGS_FILE, settings);
479
+ }
44
480
  function profileCommand() {
45
- const profile = new Command("profile").description("Manage Claude CLI profiles");
46
- profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID (e.g. claude-opus-4-6)").option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL (e.g. https://api.anthropic.com)").action((name, opts) => {
481
+ const profile = new Command2("profile").description("Manage Claude CLI profiles");
482
+ profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID (e.g. claude-opus-4-6) - can be used multiple times", collect, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL (e.g. https://api.anthropic.com)").option("-p, --provider <provider>", "Provider type: anthropic (default) or openai").action((name, opts) => {
47
483
  ensureProfilesFile();
48
484
  const data = readJson(PROFILES_FILE);
49
485
  const profile2 = data.profiles[name] || {};
50
- if (opts.model) profile2.model = opts.model;
486
+ const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
487
+ if (models) {
488
+ profile2.models = models;
489
+ profile2.model = models[0];
490
+ }
51
491
  if (opts.token) profile2.token = opts.token;
52
492
  if (opts.url) profile2.url = opts.url;
493
+ if (opts.provider) profile2.provider = opts.provider;
53
494
  data.profiles[name] = profile2;
54
495
  writeJson(PROFILES_FILE, data);
55
496
  console.log(`Profile '${name}' saved.`);
56
497
  });
57
- profile.command("update").description("Update fields of an existing profile").argument("<name>", "Profile name (must already exist)").option("-m, --model <model>", "Model ID").option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").action((name, opts) => {
498
+ profile.command("update").description("Update fields of an existing profile").argument("<name>", "Profile name (must already exist)").option("-m, --model <model>", "Model ID - can be used multiple times to set multiple models", collect, []).option("-d, --delete-model <model>", "Remove model ID - can be used multiple times", collect, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").option("-p, --provider <provider>", "Provider type: anthropic (default) or openai").action((name, opts) => {
58
499
  ensureProfilesFile();
59
500
  const data = readJson(PROFILES_FILE);
60
501
  if (!data.profiles[name]) {
@@ -62,12 +503,81 @@ function profileCommand() {
62
503
  process.exit(1);
63
504
  }
64
505
  const p = data.profiles[name];
65
- if (opts.model) p.model = opts.model;
506
+ const providedModels = opts.model && opts.model.length > 0 ? opts.model : void 0;
507
+ const modelsToDelete = opts.deleteModel && opts.deleteModel.length > 0 ? opts.deleteModel : void 0;
508
+ if (modelsToDelete) {
509
+ const toRemove = new Set(modelsToDelete);
510
+ const currentModels = p.models || (p.model ? [p.model] : []);
511
+ const newModels = currentModels.filter((m) => !toRemove.has(m));
512
+ const removedCount = currentModels.length - newModels.length;
513
+ if (removedCount === 0) {
514
+ console.log(`No matching models to remove from profile '${name}'.`);
515
+ } else if (newModels.length === 0) {
516
+ delete p.models;
517
+ delete p.model;
518
+ console.log(`Removed all models from profile '${name}'.`);
519
+ } else {
520
+ p.models = newModels;
521
+ p.model = newModels[0];
522
+ console.log(`Removed ${removedCount} model(s) from profile '${name}'.`);
523
+ }
524
+ }
525
+ if (providedModels) {
526
+ if (providedModels.length === 1) {
527
+ const modelToSet = providedModels[0];
528
+ const currentModels = p.models || (p.model ? [p.model] : []);
529
+ const existingIndex = currentModels.indexOf(modelToSet);
530
+ if (existingIndex !== -1) {
531
+ currentModels.splice(existingIndex, 1);
532
+ currentModels.unshift(modelToSet);
533
+ p.models = currentModels;
534
+ p.model = modelToSet;
535
+ console.log(`Selected existing model '${modelToSet}' (position ${existingIndex + 1} -> 1).`);
536
+ } else {
537
+ currentModels.unshift(modelToSet);
538
+ p.models = currentModels;
539
+ p.model = modelToSet;
540
+ console.log(`Added and selected new model '${modelToSet}'.`);
541
+ }
542
+ } else {
543
+ p.models = providedModels;
544
+ p.model = providedModels[0];
545
+ }
546
+ }
66
547
  if (opts.token) p.token = opts.token;
67
548
  if (opts.url) p.url = opts.url;
549
+ if (opts.provider) p.provider = opts.provider;
68
550
  writeJson(PROFILES_FILE, data);
69
551
  console.log(`Profile '${name}' updated.`);
70
552
  });
553
+ profile.command("remove-model").description("Remove specific models from a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID to remove - can be used multiple times", collect, []).action((name, opts) => {
554
+ ensureProfilesFile();
555
+ const data = readJson(PROFILES_FILE);
556
+ if (!data.profiles[name]) {
557
+ console.error(`Profile '${name}' not found.`);
558
+ process.exit(1);
559
+ }
560
+ const p = data.profiles[name];
561
+ const toRemove = new Set(opts.model);
562
+ if (toRemove.size === 0) {
563
+ console.error("No models specified to remove. Use -m <model> to specify models.");
564
+ process.exit(1);
565
+ }
566
+ const currentModels = p.models || (p.model ? [p.model] : []);
567
+ const newModels = currentModels.filter((m) => !toRemove.has(m));
568
+ if (newModels.length === 0) {
569
+ delete p.models;
570
+ delete p.model;
571
+ console.log(`Removed all models from profile '${name}'.`);
572
+ } else {
573
+ const removedCount = currentModels.length - newModels.length;
574
+ p.models = newModels;
575
+ p.model = newModels[0];
576
+ console.log(`Removed ${removedCount} model(s) from profile '${name}'.`);
577
+ console.log(`Remaining models: ${newModels.join(", ")}`);
578
+ }
579
+ writeJson(PROFILES_FILE, data);
580
+ });
71
581
  profile.command("list").description("List all profiles").action(() => {
72
582
  ensureProfilesFile();
73
583
  const data = readJson(PROFILES_FILE);
@@ -78,17 +588,18 @@ function profileCommand() {
78
588
  return;
79
589
  }
80
590
  const def = data.default || "";
81
- const fmt = (marker, name, model, token, url) => `${marker.padEnd(2)} ${name.padEnd(20)} ${model.padEnd(30)} ${token.padEnd(20)} ${url}`;
82
- console.log(fmt("", "NAME", "MODEL", "TOKEN", "URL"));
83
- console.log(fmt("", "----", "-----", "-----", "---"));
591
+ const fmt = (marker, name, model, token, provider, url) => `${marker.padEnd(2)} ${name.padEnd(20)} ${model.padEnd(30)} ${token.padEnd(20)} ${provider.padEnd(12)} ${url}`;
592
+ console.log(fmt("", "NAME", "MODEL(S)", "TOKEN", "PROVIDER", "URL"));
593
+ console.log(fmt("", "----", "--------", "-----", "--------", "---"));
84
594
  for (const name of names) {
85
595
  const p = profiles[name];
86
596
  const marker = name === def ? "* " : " ";
87
597
  console.log(fmt(
88
598
  marker,
89
599
  name,
90
- p.model || "(unset)",
600
+ formatModels(p),
91
601
  maskToken(p.token || ""),
602
+ p.provider || "anthropic",
92
603
  p.url || "(default)"
93
604
  ));
94
605
  }
@@ -104,10 +615,27 @@ function profileCommand() {
104
615
  if (opts.json) {
105
616
  console.log(JSON.stringify({ name, ...p }, null, 2));
106
617
  } else {
107
- console.log(`Name: ${name}`);
108
- console.log(`Model: ${p.model || "(unset)"}`);
109
- console.log(`Token: ${p.token || "(unset)"}`);
110
- console.log(`URL: ${p.url || "(default)"}`);
618
+ console.log(`Name: ${name}`);
619
+ console.log(`Model: ${p.model || "(unset)"}`);
620
+ if (p.models && p.models.length > 0) {
621
+ const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel(m));
622
+ console.log(`Models:`);
623
+ for (const m of p.models) {
624
+ if (!isAnthropicModel(m)) {
625
+ const aliasIndex = nonAnthropicModels.indexOf(m);
626
+ let alias = "";
627
+ if (aliasIndex === 0) alias = " (opus)";
628
+ else if (aliasIndex === 1) alias = " (sonnet)";
629
+ else if (aliasIndex === 2) alias = " (haiku)";
630
+ console.log(` - ${m}${alias}`);
631
+ } else {
632
+ console.log(` - ${m}`);
633
+ }
634
+ }
635
+ }
636
+ console.log(`Token: ${p.token || "(unset)"}`);
637
+ console.log(`URL: ${p.url || "(default)"}`);
638
+ console.log(`Provider: ${p.provider || "anthropic"}`);
111
639
  }
112
640
  });
113
641
  profile.command("remove").description("Remove a profile").argument("<name>", "Profile name").action((name) => {
@@ -134,25 +662,70 @@ function profileCommand() {
134
662
  });
135
663
  return profile;
136
664
  }
665
+ function collect(value, previous) {
666
+ return previous.concat([value]);
667
+ }
137
668
  function execClaude(profileName, p, extraArgs) {
669
+ updateSettingsForProfile(p);
670
+ const models = p.models || (p.model ? [p.model] : []);
671
+ const firstModel = models[0];
138
672
  const cmd = ["claude"];
139
- if (p.model) cmd.push("--model", p.model);
673
+ if (firstModel) cmd.push("--model", firstModel);
140
674
  cmd.push(...extraArgs);
141
675
  const env = {
142
676
  ...process.env,
143
677
  ANTHROPIC_AUTH_TOKEN: p.token || void 0,
144
678
  ANTHROPIC_BASE_URL: p.url || void 0
145
679
  };
680
+ const nonAnthropicModels = models.filter((m) => !isAnthropicModel(m));
681
+ if (nonAnthropicModels.length > 0) {
682
+ if (nonAnthropicModels[0]) {
683
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = nonAnthropicModels[0];
684
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME = nonAnthropicModels[0];
685
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION = `Custom: ${nonAnthropicModels[0]}`;
686
+ }
687
+ if (nonAnthropicModels[1]) {
688
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = nonAnthropicModels[1];
689
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME = nonAnthropicModels[1];
690
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION = `Custom: ${nonAnthropicModels[1]}`;
691
+ }
692
+ if (nonAnthropicModels[2]) {
693
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = nonAnthropicModels[2];
694
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME = nonAnthropicModels[2];
695
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION = `Custom: ${nonAnthropicModels[2]}`;
696
+ }
697
+ env.ANTHROPIC_CUSTOM_MODEL_OPTION = nonAnthropicModels[0];
698
+ }
146
699
  delete env.ANTHROPIC_API_KEY;
147
- console.error(`Using profile '${profileName}': model=${p.model || "(default)"} url=${p.url || "(default)"}`);
148
- const result = spawnSync(cmd[0], cmd.slice(1), {
149
- stdio: "inherit",
150
- env
151
- });
152
- process.exit(result.status ?? 1);
700
+ console.error(`Using profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"}`);
701
+ if (p.provider === "openai") {
702
+ const allModels = p.models || (p.model ? [p.model] : []);
703
+ startOpenAIProxy(
704
+ p.url || "https://api.openai.com",
705
+ p.token || "",
706
+ firstModel || "gpt-4o",
707
+ allModels
708
+ ).then(({ baseUrl, stop }) => {
709
+ env.ANTHROPIC_BASE_URL = baseUrl;
710
+ const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env });
711
+ child.on("close", (code) => {
712
+ stop();
713
+ process.exit(code ?? 1);
714
+ });
715
+ }).catch((err) => {
716
+ console.error("Failed to start OpenAI proxy:", err);
717
+ process.exit(1);
718
+ });
719
+ } else {
720
+ const result = spawnSync(cmd[0], cmd.slice(1), {
721
+ stdio: "inherit",
722
+ env
723
+ });
724
+ process.exit(result.status ?? 1);
725
+ }
153
726
  }
154
727
  function useCommand() {
155
- return new Command("use").description("Set a profile as the default").argument("<name>", "Profile name").action((name) => {
728
+ return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action((name) => {
156
729
  ensureProfilesFile();
157
730
  const data = readJson(PROFILES_FILE);
158
731
  if (!data.profiles[name]) {
@@ -165,7 +738,8 @@ function useCommand() {
165
738
  });
166
739
  }
167
740
  function runCommand() {
168
- return new Command("run").description("Launch Claude Code using the default or a specified profile").allowUnknownOption().argument("[args...]", "Optional profile name followed by extra arguments").action((args) => {
741
+ return new Command2("run").description("Launch Claude Code using the default or a specified profile").allowUnknownOption().argument("[args...]", "Optional profile name followed by extra arguments").action((args) => {
742
+ fixJsonFile(CLAUDE_JSON);
169
743
  ensureProfilesFile();
170
744
  const data = readJson(PROFILES_FILE);
171
745
  let profileName = "";
@@ -187,7 +761,7 @@ function runCommand() {
187
761
  }
188
762
 
189
763
  // src/hooks.ts
190
- import { Command as Command2 } from "commander";
764
+ import { Command as Command3 } from "commander";
191
765
  function buildFlat(data) {
192
766
  const rows = [];
193
767
  const hooksRoot = data.hooks || {};
@@ -227,7 +801,7 @@ function buildFlat(data) {
227
801
  return rows;
228
802
  }
229
803
  function hooksCommand() {
230
- const hooks = new Command2("hook").description("Manage Claude Code hooks in settings.json");
804
+ const hooks = new Command3("hook").description("Manage Claude Code hooks in settings.json");
231
805
  hooks.command("list").description("List all hooks").action(() => {
232
806
  ensureSettingsFile();
233
807
  const data = readJson(SETTINGS_FILE);
@@ -379,7 +953,7 @@ function hooksCommand() {
379
953
  }
380
954
 
381
955
  // src/sessions.ts
382
- import { Command as Command3 } from "commander";
956
+ import { Command as Command4 } from "commander";
383
957
  import fs2 from "fs";
384
958
  import path2 from "path";
385
959
  import { execSync } from "child_process";
@@ -463,7 +1037,7 @@ function snippet(text, query, width = 150) {
463
1037
  return prefix + text.slice(start, end) + suffix;
464
1038
  }
465
1039
  function sessionCommand() {
466
- const session = new Command3("session").description("Manage Claude Code sessions");
1040
+ const session = new Command4("session").description("Manage Claude Code sessions");
467
1041
  session.command("list").description("List all Claude Code project sessions").option("-n, --limit <n>", "Max number of projects to show", "30").option("-s, --short", "Show encoded names only (no decoding)").option("-j, --json", "Output as JSON lines").action((opts) => {
468
1042
  const limit = parseInt(opts.limit, 10);
469
1043
  let dirs;
@@ -755,7 +1329,7 @@ function sessionCommand() {
755
1329
  }
756
1330
 
757
1331
  // src/complete.ts
758
- import { Command as Command4 } from "commander";
1332
+ import { Command as Command5 } from "commander";
759
1333
  var ZSH_COMPLETION = `#compdef cc-hub
760
1334
 
761
1335
  _cc-hub() {
@@ -766,14 +1340,22 @@ _cc-hub() {
766
1340
  'run:Launch Claude Code using the default or a specified profile'
767
1341
  'hook:Manage Claude Code hooks in settings.json'
768
1342
  'session:Manage Claude Code sessions'
1343
+ 'provider:Manage provider types'
769
1344
  'complete:Print shell completion functions'
770
1345
  'help:Display help for a command'
771
1346
  )
772
1347
 
1348
+ local -a provider_subcmds
1349
+ provider_subcmds=(
1350
+ 'list:List available provider types'
1351
+ )
1352
+
1353
+
773
1354
  local -a profile_subcmds
774
1355
  profile_subcmds=(
775
1356
  'add:Add or update a profile'
776
1357
  'update:Update fields of an existing profile'
1358
+ 'remove-model:Remove specific models from a profile'
777
1359
  'list:List all profiles'
778
1360
  'view:View full details of a profile'
779
1361
  'remove:Remove a profile'
@@ -803,16 +1385,21 @@ _cc-hub() {
803
1385
  local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
804
1386
  if [[ -f "$profiles_file" ]]; then
805
1387
  local -a names
806
- names=(\${(f)"$(command python3 -c "
807
- import json
808
- data = json.load(open('$profiles_file'))
809
- for name in data.get('profiles', {}):
810
- print(name)
811
- " 2>/dev/null)"})
1388
+ names=(\${(f)"$(command jq -r '.profiles | keys[]' "$profiles_file" 2>/dev/null)"})
812
1389
  _describe -t profiles 'profile' names
813
1390
  fi
814
1391
  }
815
1392
 
1393
+ _cc_hub_models_for_profile() {
1394
+ local profile_name="$1"
1395
+ local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
1396
+ if [[ -f "$profiles_file" && -n "$profile_name" ]]; then
1397
+ local -a models
1398
+ models=(\${(f)"$(command jq -r --arg p "$profile_name" '(.profiles[$p].models // [ .profiles[$p].model ] )[]? // empty' "$profiles_file" 2>/dev/null)"})
1399
+ _describe -t models 'model' models
1400
+ fi
1401
+ }
1402
+
816
1403
  _arguments -C \\
817
1404
  '1: :->command' \\
818
1405
  '*::arg:->args'
@@ -826,8 +1413,26 @@ for name in data.get('profiles', {}):
826
1413
  profile)
827
1414
  if (( CURRENT == 2 )); then
828
1415
  _describe -t profile-subcmds 'profile subcommand' profile_subcmds
829
- elif [[ $words[2] == "view" || $words[2] == "remove" || $words[2] == "default" || $words[2] == "update" ]]; then
1416
+ elif [[ $words[2] == "view" || $words[2] == "remove" || $words[2] == "default" || $words[2] == "remove-model" ]]; then
830
1417
  _cc_hub_profiles
1418
+ elif [[ $words[2] == "update" ]]; then
1419
+ if (( CURRENT == 3 )); then
1420
+ _cc_hub_profiles
1421
+ else
1422
+ words=("stub" $words[3,-1])
1423
+ (( CURRENT-- ))
1424
+ _arguments -C -S '1:profile:_cc_hub_profiles' '(-m --model)*'{-m,--model}'[Model ID]:model:->profileModel' '(-d --delete-model)*'{-d,--delete-model}'[Remove model ID]:model:->profileModel' '(-t --token)'{-t,--token}'[API key / token]:token:' '(-u --url)'{-u,--url}'[Base URL]:url:' '(-p --provider)'{-p,--provider}'[Provider type]:provider:(anthropic openai)'
1425
+ case $state in
1426
+ profileModel)
1427
+ _cc_hub_models_for_profile $line[1]
1428
+ ;;
1429
+ esac
1430
+ fi
1431
+ fi
1432
+ ;;
1433
+ provider)
1434
+ if (( CURRENT == 2 )); then
1435
+ _describe -t provider-subcmds 'provider subcommand' provider_subcmds
831
1436
  fi
832
1437
  ;;
833
1438
  use|run)
@@ -864,14 +1469,39 @@ for name in data.get('profiles', {}):
864
1469
  fi
865
1470
  }
866
1471
 
1472
+ _cc-hub_models_for_profile() {
1473
+ local profile_name="$1"
1474
+ local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
1475
+ if [[ -f "$profiles_file" && -n "$profile_name" ]]; then
1476
+ local models
1477
+ models=$(command python3 -c "
1478
+ import json
1479
+ data = json.load(open('$profiles_file'))
1480
+ p = data.get('profiles', {}).get('$profile_name', {})
1481
+ models = p.get('models')
1482
+ if isinstance(models, list):
1483
+ for m in models:
1484
+ if m:
1485
+ print(m)
1486
+ else:
1487
+ m = p.get('model')
1488
+ if m:
1489
+ print(m)
1490
+ " 2>/dev/null)
1491
+ COMPREPLY=($(compgen -W "$models" -- "\${cur}"))
1492
+ fi
1493
+ }
1494
+
867
1495
  _cc-hub() {
868
1496
  local cur prev commands
869
1497
  COMPREPLY=()
870
1498
  cur="\${COMP_WORDS[COMP_CWORD]}"
871
1499
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
872
- commands="profile use run hook session complete help"
1500
+ commands="profile use run hook session provider complete help"
873
1501
 
874
- local profile_subcmds="add update list view remove default"
1502
+ local profile_subcmds="add update remove-model list view remove default"
1503
+ local provider_subcmds="list"
1504
+ local provider_types="anthropic openai"
875
1505
  local hooks_subcmds="list add remove enable disable"
876
1506
  local session_subcmds="list show search ps stats clean"
877
1507
 
@@ -887,10 +1517,28 @@ _cc-hub() {
887
1517
  profile)
888
1518
  if [[ \${COMP_CWORD} -eq 2 ]]; then
889
1519
  COMPREPLY=($(compgen -W "$profile_subcmds" -- "$cur"))
890
- elif [[ "$prev" == "view" || "$prev" == "remove" || "$prev" == "default" || "$prev" == "update" ]]; then
1520
+ elif [[ "$prev" == "view" || "$prev" == "remove" || "$prev" == "default" || "$prev" == "remove-model" ]]; then
891
1521
  _cc-hub_profiles
892
1522
  elif [[ "$prev" == "profile" ]]; then
893
1523
  COMPREPLY=($(compgen -W "$profile_subcmds" -- "$cur"))
1524
+ elif [[ "\${COMP_WORDS[2]}" == "update" && \${COMP_CWORD} -eq 3 ]]; then
1525
+ _cc-hub_profiles
1526
+ elif [[ "\${COMP_WORDS[2]}" == "update" ]]; then
1527
+ if [[ "$prev" == "--provider" || "$prev" == "-p" ]]; then
1528
+ COMPREPLY=($(compgen -W "$provider_types" -- "$cur"))
1529
+ elif [[ "$prev" == "--model" || "$prev" == "-m" || "$prev" == "--delete-model" || "$prev" == "-d" ]]; then
1530
+ _cc-hub_models_for_profile "\${COMP_WORDS[3]}"
1531
+ else
1532
+ local update_opts="--model -m --delete-model -d --token -t --url -u --provider -p"
1533
+ COMPREPLY=($(compgen -W "$update_opts" -- "$cur"))
1534
+ fi
1535
+ fi
1536
+ ;;
1537
+ provider)
1538
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
1539
+ COMPREPLY=($(compgen -W "$provider_subcmds" -- "$cur"))
1540
+ elif [[ "$prev" == "provider" ]]; then
1541
+ COMPREPLY=($(compgen -W "$provider_subcmds" -- "$cur"))
894
1542
  fi
895
1543
  ;;
896
1544
  use|run)
@@ -914,7 +1562,7 @@ _cc-hub() {
914
1562
  complete -F _cc-hub cc-hub
915
1563
  `;
916
1564
  function completeCommand() {
917
- return new Command4("complete").description("Print shell completion script").argument("<shell>", "Shell type: bash or zsh").action((shell) => {
1565
+ return new Command5("complete").description("Print shell completion script").argument("<shell>", "Shell type: bash or zsh").action((shell) => {
918
1566
  switch (shell) {
919
1567
  case "zsh":
920
1568
  process.stdout.write(ZSH_COMPLETION);
@@ -930,7 +1578,7 @@ function completeCommand() {
930
1578
  }
931
1579
 
932
1580
  // src/index.ts
933
- var program = new Command5();
1581
+ var program = new Command6();
934
1582
  program.name("cc-hub").description("Manage Claude CLI profiles, hooks, and sessions").version("1.0.0");
935
1583
  program.addCommand(profileCommand());
936
1584
  program.addCommand(useCommand());
@@ -938,4 +1586,5 @@ program.addCommand(runCommand());
938
1586
  program.addCommand(hooksCommand());
939
1587
  program.addCommand(sessionCommand());
940
1588
  program.addCommand(completeCommand());
1589
+ program.addCommand(providerCommand());
941
1590
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {