cc-hub-cli 1.1.5 → 1.1.7

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 +38 -0
  2. package/dist/index.js +302 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -159,6 +159,44 @@ Invoke-Expression (& cc-hub completion powershell | Out-String)
159
159
 
160
160
  Completes subcommands, profile names, and event types.
161
161
 
162
+ ## Logging
163
+
164
+ cc-hub writes structured logs to `~/.claude/cc-hub/logs/cc-hub-YYYY-MM-DD.log`.
165
+
166
+ ### Log levels
167
+
168
+ Levels are ordered from most to least verbose:
169
+
170
+ | Level | Description |
171
+ |---|---|
172
+ | `DEBUG` | All service calls, file reads/writes, path encode/decode, proxy requests |
173
+ | `INFO` | Command executions, profile launches, proxy start/stop |
174
+ | `WARN` | JSON auto-fix events, backup restores |
175
+ | `ERROR` | Thrown exceptions, upstream errors, uncaught exceptions/rejections |
176
+
177
+ The default level is `INFO`. To change it, add `_cc_hub_logLevel` to `~/.claude/settings.json`:
178
+
179
+ ```json
180
+ {
181
+ "_cc_hub_logLevel": "DEBUG"
182
+ }
183
+ ```
184
+
185
+ Valid values: `DEBUG`, `INFO`, `WARN`, `ERROR`.
186
+
187
+ ### Viewing logs
188
+
189
+ ```bash
190
+ # Tail today's log
191
+ tail -f ~/.claude/cc-hub/logs/cc-hub-$(date +%Y-%m-%d).log
192
+
193
+ # View all logs
194
+ ls -lt ~/.claude/cc-hub/logs/
195
+
196
+ # Search for errors
197
+ grep ERROR ~/.claude/cc-hub/logs/*.log
198
+ ```
199
+
162
200
  ## Configuration
163
201
 
164
202
  cc-hub reads from these paths (overridable via environment variables):
package/dist/index.js CHANGED
@@ -254,6 +254,14 @@ var NoOpDesktopApp = class {
254
254
  import fs3 from "fs";
255
255
  import path3 from "path";
256
256
  import { randomUUID } from "crypto";
257
+ var ANTHROPIC_ALIASES = ["claude-sonnet-4-5", "claude-opus-4-7", "claude-haiku-4-5-20251001"];
258
+ function isAnthropicModel(model) {
259
+ const anthropicAliases = ["opus", "sonnet", "haiku", "best", "default", "opusplan", "opus[1m]", "sonnet[1m]"];
260
+ const lower = model.toLowerCase();
261
+ if (anthropicAliases.includes(lower)) return true;
262
+ if (lower.startsWith("claude-")) return true;
263
+ return false;
264
+ }
257
265
  function toDesktopProfile(p) {
258
266
  const models = p.models || (p.model ? [p.model] : []);
259
267
  const isAnthropic = p.provider === "anthropic" || !p.provider && !p.url;
@@ -263,13 +271,24 @@ function toDesktopProfile(p) {
263
271
  inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
264
272
  };
265
273
  }
266
- return {
274
+ const mappings = [];
275
+ const mappedModels = models.map((m, index) => {
276
+ if (isAnthropicModel(m)) return m;
277
+ const alias = ANTHROPIC_ALIASES[Math.min(index, ANTHROPIC_ALIASES.length - 1)];
278
+ mappings.push({ alias, actual: m });
279
+ return alias;
280
+ });
281
+ const result = {
267
282
  inferenceProvider: "gateway",
268
283
  inferenceGatewayBaseUrl: p.url || void 0,
269
284
  inferenceGatewayApiKey: p.token || void 0,
270
285
  inferenceGatewayAuthScheme: "bearer",
271
- inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
286
+ inferenceModels: mappedModels.map((m) => ({ name: m, supports1m: true }))
272
287
  };
288
+ if (mappings.length > 0) {
289
+ result.inferenceModelMappings = mappings;
290
+ }
291
+ return result;
273
292
  }
274
293
  var DesktopProfileSyncer = class {
275
294
  constructor(app) {
@@ -471,6 +490,7 @@ function fixJsonFile(filePath, fallback = {}) {
471
490
  const raw = fs4.readFileSync(filePath, "utf-8");
472
491
  try {
473
492
  JSON.parse(raw);
493
+ fs4.mkdirSync(CLAUDE_DIR, { recursive: true });
474
494
  fs4.copyFileSync(filePath, backupPath);
475
495
  return;
476
496
  } catch {
@@ -500,14 +520,24 @@ function fixJsonFile(filePath, fallback = {}) {
500
520
  warn(`Fixed invalid JSON in ${path4.basename(filePath)}.`);
501
521
  console.error(`Fixed invalid JSON in ${path4.basename(filePath)}.`);
502
522
  } catch {
523
+ let restored = false;
503
524
  if (fs4.existsSync(backupPath)) {
504
- fs4.copyFileSync(backupPath, filePath);
505
- warn(`Restored ${path4.basename(filePath)} from backup.`);
506
- console.error(`Restored ${path4.basename(filePath)} from backup.`);
507
- } else {
525
+ try {
526
+ const backupRaw = fs4.readFileSync(backupPath, "utf-8");
527
+ JSON.parse(backupRaw);
528
+ fs4.copyFileSync(backupPath, filePath);
529
+ restored = true;
530
+ warn(`Restored ${path4.basename(filePath)} from backup.`);
531
+ console.error(`Restored ${path4.basename(filePath)} from backup.`);
532
+ } catch {
533
+ error(`Backup ${path4.basename(backupPath)} is also corrupt; using fallback.`);
534
+ console.error(`Backup ${path4.basename(backupPath)} is also corrupt; using fallback.`);
535
+ }
536
+ }
537
+ if (!restored) {
508
538
  writeJson(filePath, fallback);
509
- error(`Could not fix ${path4.basename(filePath)}, no backup found, reset to default.`);
510
- console.error(`Could not fix ${path4.basename(filePath)}, no backup found, reset to default.`);
539
+ error(`Could not fix ${path4.basename(filePath)}, no valid backup found, reset to default.`);
540
+ console.error(`Could not fix ${path4.basename(filePath)}, no valid backup found, reset to default.`);
511
541
  }
512
542
  }
513
543
  }
@@ -518,6 +548,9 @@ import { spawnSync as spawnSync2, spawn } from "child_process";
518
548
  // src/provider/index.ts
519
549
  import { Command } from "commander";
520
550
 
551
+ // src/provider/server.ts
552
+ import http from "http";
553
+
521
554
  // src/provider/transform.ts
522
555
  function sanitizeToolId(id) {
523
556
  let sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -558,17 +591,26 @@ function transformAnthropicToOpenAI(body) {
558
591
  });
559
592
  }
560
593
  const contentParts = msg.content.filter(
561
- (b) => b.type === "text" && b.text || b.type === "image" && b.source
594
+ (b) => b.type === "text" && b.text || b.type === "image" && (b.source?.type === "base64" && b.source.media_type && b.source.data || b.source?.type === "url" && b.source.url)
562
595
  );
563
596
  if (contentParts.length > 0) {
564
597
  const converted = contentParts.map((part) => {
565
598
  if (part.type === "image") {
566
- const url = part.source?.type === "base64" ? `data:${part.source.media_type};base64,${part.source.data}` : part.source?.url ?? "";
567
- return { type: "image_url", image_url: { url } };
599
+ if (part.source?.type === "base64" && part.source.media_type && part.source.data) {
600
+ const url = `data:${part.source.media_type};base64,${part.source.data}`;
601
+ debug(`transform: converting base64 image (${part.source.media_type}, ${part.source.data.length} chars)`);
602
+ return { type: "image_url", image_url: { url } };
603
+ } else if (part.source?.type === "url" && part.source.url) {
604
+ debug(`transform: converting image url (${part.source.url.slice(0, 80)}...)`);
605
+ return { type: "image_url", image_url: { url: part.source.url } };
606
+ }
607
+ warn(`transform: skipping invalid image block (missing source fields)`);
608
+ return null;
568
609
  }
569
610
  return { type: "text", text: part.text };
570
- });
571
- if (converted.every((p) => p.type === "text")) {
611
+ }).filter(Boolean);
612
+ if (converted.length === 0) {
613
+ } else if (converted.every((p) => p.type === "text")) {
572
614
  messages.push({
573
615
  role: "user",
574
616
  content: converted.map((p) => p.text).join("")
@@ -666,7 +708,7 @@ function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
666
708
  id: openaiResponse.id ?? `msg_${Date.now()}`,
667
709
  type: "message",
668
710
  role: "assistant",
669
- model: openaiResponse.model ?? originalModel,
711
+ model: originalModel,
670
712
  content,
671
713
  stop_reason: finishMap[choice.finish_reason] ?? "end_turn",
672
714
  stop_sequence: null,
@@ -734,8 +776,7 @@ data: ${JSON.stringify(data)}
734
776
  }
735
777
 
736
778
  // src/provider/server.ts
737
- import http from "http";
738
- async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
779
+ async function startOpenAIProxy(targetUrl, apiKey, model, models = [], modelMappings = {}) {
739
780
  const base = targetUrl.replace(/\/+$/, "");
740
781
  const server = http.createServer(async (req, res) => {
741
782
  debug(`Proxy request: ${req.method} ${req.url}`);
@@ -761,7 +802,9 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
761
802
  return;
762
803
  }
763
804
  const isStream = !!parsed.stream;
764
- const openaiBody = transformAnthropicToOpenAI({ ...parsed, stream: false });
805
+ const requestModel = parsed.model ?? model;
806
+ const actualModel = modelMappings[requestModel] || requestModel;
807
+ const openaiBody = transformAnthropicToOpenAI({ ...parsed, model: actualModel, stream: false });
765
808
  if (isStream) {
766
809
  res.writeHead(200, {
767
810
  "Content-Type": "text/event-stream",
@@ -866,6 +909,17 @@ var PROVIDERS = [
866
909
  description: "Embedded proxy \u2014 translates Anthropic requests to OpenAI Chat Completions format"
867
910
  }
868
911
  ];
912
+ var ANTHROPIC_ALIASES2 = ["claude-sonnet-4-5", "claude-opus-4-7", "claude-haiku-4-5-20251001"];
913
+ function isAnthropicModel2(model) {
914
+ const anthropicAliases = ["opus", "sonnet", "haiku", "best", "default", "opusplan", "opus[1m]", "sonnet[1m]"];
915
+ const lower = model.toLowerCase();
916
+ if (anthropicAliases.includes(lower)) return true;
917
+ if (lower.startsWith("claude-")) return true;
918
+ return false;
919
+ }
920
+ function collect(value, previous) {
921
+ return previous.concat([value]);
922
+ }
869
923
  function providerCommand() {
870
924
  const cmd = new Command("provider").description("Manage provider types");
871
925
  cmd.command("list").description("List available provider types").action(safeAction(() => {
@@ -878,8 +932,51 @@ function providerCommand() {
878
932
  }));
879
933
  return cmd;
880
934
  }
935
+ function proxyCommand() {
936
+ return new Command("proxy").description("Start a standalone OpenAI proxy for the desktop app").option("--profile <name>", "Use configuration from a saved profile").option("-u, --url <url>", "Upstream base URL (e.g., https://api.openai.com)").option("-k, --api-key <key>", "Upstream API key").option("-m, --model <model>", "Default model", "gpt-4o").option("--mapping <mapping>", "Model alias mapping (format: alias:actual, can be used multiple times)", collect, []).action(safeAction(async (opts) => {
937
+ let targetUrl = opts.url || "https://api.openai.com";
938
+ let apiKey = opts.apiKey || "";
939
+ let defaultModel = opts.model || "gpt-4o";
940
+ let models = [];
941
+ const modelMappings = {};
942
+ if (opts.profile) {
943
+ ensureProfilesFile();
944
+ const data = readJson(PROFILES_FILE);
945
+ const p = data.profiles[opts.profile];
946
+ if (!p) {
947
+ throw new Error(`Profile '${opts.profile}' not found.`);
948
+ }
949
+ targetUrl = p.url || targetUrl;
950
+ apiKey = p.token || apiKey;
951
+ models = p.models || (p.model ? [p.model] : []);
952
+ defaultModel = models[0] || defaultModel;
953
+ models.forEach((m, i) => {
954
+ if (!isAnthropicModel2(m)) {
955
+ const alias = ANTHROPIC_ALIASES2[Math.min(i, ANTHROPIC_ALIASES2.length - 1)];
956
+ modelMappings[alias] = m;
957
+ }
958
+ });
959
+ } else {
960
+ for (const m of opts.mapping) {
961
+ const [alias, actual] = m.split(":");
962
+ if (alias && actual) {
963
+ modelMappings[alias] = actual;
964
+ }
965
+ }
966
+ models = [defaultModel];
967
+ }
968
+ const { baseUrl, stop } = await startOpenAIProxy(targetUrl, apiKey, defaultModel, models, modelMappings);
969
+ console.log(`Proxy running at ${baseUrl}`);
970
+ console.log("Press Ctrl+C to stop");
971
+ process.on("SIGINT", () => {
972
+ stop();
973
+ process.exit(0);
974
+ });
975
+ }));
976
+ }
881
977
 
882
978
  // src/profiles/runner.ts
979
+ var BUILT_IN_DEFAULT = "__builtin__";
883
980
  function resolveClaudeBinary() {
884
981
  return createBinaryResolver().resolve();
885
982
  }
@@ -953,7 +1050,8 @@ function execClaude(profileName, p, extraArgs) {
953
1050
  p.url || "https://api.openai.com",
954
1051
  p.token || "",
955
1052
  firstModel || "gpt-4o",
956
- allModels
1053
+ allModels,
1054
+ {}
957
1055
  ).then(({ baseUrl, stop }) => {
958
1056
  env.ANTHROPIC_BASE_URL = baseUrl;
959
1057
  debug(`execClaude: proxy running at ${baseUrl}`);
@@ -976,6 +1074,22 @@ function execClaude(profileName, p, extraArgs) {
976
1074
  process.exit(result.status ?? 1);
977
1075
  }
978
1076
  }
1077
+ function execClaudeBuiltIn(extraArgs) {
1078
+ const binary = resolveClaudeBinary();
1079
+ const cmd = [binary, ...extraArgs];
1080
+ const env = {
1081
+ ...process.env,
1082
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
1083
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1"
1084
+ };
1085
+ info(`Launching Claude with built-in official models: binary=${binary}`);
1086
+ const result = spawnSync2(cmd[0], cmd.slice(1), {
1087
+ stdio: "inherit",
1088
+ env,
1089
+ shell: process.platform === "win32"
1090
+ });
1091
+ process.exit(result.status ?? 1);
1092
+ }
979
1093
 
980
1094
  // src/profiles/commands.ts
981
1095
  function maskToken(token) {
@@ -985,10 +1099,10 @@ function maskToken(token) {
985
1099
  }
986
1100
  function formatModels(p) {
987
1101
  if (p.models && p.models.length > 0) {
988
- const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel(m));
1102
+ const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel3(m));
989
1103
  const parts = [];
990
1104
  p.models.forEach((m, i) => {
991
- if (!isAnthropicModel(m)) {
1105
+ if (!isAnthropicModel3(m)) {
992
1106
  const aliasIndex = nonAnthropicModels.indexOf(m);
993
1107
  if (aliasIndex === 0) parts.push(`${m} (sonnet)`);
994
1108
  else if (aliasIndex === 1) parts.push(`${m} (opus)`);
@@ -1006,20 +1120,20 @@ function formatModels(p) {
1006
1120
  }
1007
1121
  return p.model || "(unset)";
1008
1122
  }
1009
- function isAnthropicModel(model) {
1123
+ function isAnthropicModel3(model) {
1010
1124
  const anthropicAliases = ["opus", "sonnet", "haiku", "best", "default", "opusplan", "opus[1m]", "sonnet[1m]"];
1011
1125
  const lower = model.toLowerCase();
1012
1126
  if (anthropicAliases.includes(lower)) return true;
1013
1127
  if (lower.startsWith("claude-")) return true;
1014
1128
  return false;
1015
1129
  }
1016
- function collect(value, previous) {
1130
+ function collect2(value, previous) {
1017
1131
  return previous.concat([value]);
1018
1132
  }
1019
1133
  function profileCommand() {
1020
1134
  const profile = new Command2("profile").description("Manage Claude CLI profiles");
1021
1135
  const syncer = createProfileSyncer();
1022
- profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID - can be used multiple times (max 3)", 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(safeAction((name, opts) => {
1136
+ profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID - can be used multiple times (max 3)", collect2, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").option("-p, --provider <provider>", "Provider type: anthropic (default) or openai").action(safeAction((name, opts) => {
1023
1137
  const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
1024
1138
  if (models && models.length > 3) {
1025
1139
  throw new Error("Error: A profile can have at most 3 models.");
@@ -1041,7 +1155,7 @@ function profileCommand() {
1041
1155
  debug(`profile add: wrote ${PROFILES_FILE}`);
1042
1156
  console.log(`Profile '${name}' saved.`);
1043
1157
  }));
1044
- 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", 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").action(safeAction((name, opts) => {
1158
+ 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", collect2, []).option("-d, --delete-model <model>", "Remove model ID - can be used multiple times", collect2, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").option("-p, --provider <provider>", "Provider type").action(safeAction((name, opts) => {
1045
1159
  ensureProfilesFile();
1046
1160
  const data = readJson(PROFILES_FILE);
1047
1161
  if (!data.profiles[name]) {
@@ -1133,6 +1247,9 @@ function profileCommand() {
1133
1247
  profile.command("view").description("View full details of a profile (token unmasked)").argument("<name>", "Profile name").option("-j, --json", "Output as JSON").action(safeAction((name, opts) => {
1134
1248
  ensureProfilesFile();
1135
1249
  const data = readJson(PROFILES_FILE);
1250
+ if (name === BUILT_IN_DEFAULT) {
1251
+ throw new Error(`'${BUILT_IN_DEFAULT}' is not a stored profile. Use 'cc-hub run --built-in' or 'cc-hub use --built-in' for official Anthropic models.`);
1252
+ }
1136
1253
  const p = data.profiles[name];
1137
1254
  if (!p) {
1138
1255
  throw new Error(`Profile '${name}' not found.`);
@@ -1144,10 +1261,10 @@ function profileCommand() {
1144
1261
  console.log(`Name: ${name}`);
1145
1262
  console.log(`Model: ${p.model || "(unset)"}`);
1146
1263
  if (p.models && p.models.length > 0) {
1147
- const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel(m));
1264
+ const nonAnthropicModels = p.models.filter((m) => !isAnthropicModel3(m));
1148
1265
  console.log(`Models:`);
1149
1266
  for (const m of p.models) {
1150
- if (!isAnthropicModel(m)) {
1267
+ if (!isAnthropicModel3(m)) {
1151
1268
  const aliasIndex = nonAnthropicModels.indexOf(m);
1152
1269
  let alias = "";
1153
1270
  if (aliasIndex === 0) alias = " (sonnet)";
@@ -1194,9 +1311,19 @@ function profileCommand() {
1194
1311
  writeJson(PROFILES_FILE, data);
1195
1312
  console.log(`Profile '${oldName}' renamed to '${newName}'.`);
1196
1313
  }));
1197
- profile.command("default").description("Set the default profile").argument("<name>", "Profile name to set as default").action(safeAction((name) => {
1314
+ profile.command("default").description("Set the default profile").option("--built-in", "Use official Anthropic models as default").argument("[name]", "Profile name to set as default (required unless --built-in)").action(safeAction((name, opts) => {
1198
1315
  ensureProfilesFile();
1199
1316
  const data = readJson(PROFILES_FILE);
1317
+ if (opts.builtIn) {
1318
+ data.default = BUILT_IN_DEFAULT;
1319
+ writeJson(PROFILES_FILE, data);
1320
+ debug(`profile default: wrote ${PROFILES_FILE}`);
1321
+ console.log("Default set to built-in official Anthropic models.");
1322
+ return;
1323
+ }
1324
+ if (!name) {
1325
+ throw new Error("Profile name is required. Use --built-in for official Anthropic models.");
1326
+ }
1200
1327
  if (!data.profiles[name]) {
1201
1328
  throw new Error(`Profile '${name}' not found.`);
1202
1329
  }
@@ -1219,6 +1346,7 @@ function profileCommand() {
1219
1346
  return;
1220
1347
  }
1221
1348
  for (const name of names) {
1349
+ if (name === BUILT_IN_DEFAULT) continue;
1222
1350
  const p = data.profiles[name];
1223
1351
  debug(`profile sync: syncing '${name}' to desktop`);
1224
1352
  syncer.sync(name, p);
@@ -1231,9 +1359,19 @@ function profileCommand() {
1231
1359
  }
1232
1360
  function useCommand() {
1233
1361
  const syncer = createProfileSyncer();
1234
- return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action(safeAction((name) => {
1362
+ return new Command2("use").description("Set a profile as the default").option("--built-in", "Use official Anthropic models as default").argument("[name]", "Profile name (required unless --built-in)").action(safeAction((name, opts) => {
1235
1363
  ensureProfilesFile();
1236
1364
  const data = readJson(PROFILES_FILE);
1365
+ if (opts.builtIn) {
1366
+ data.default = BUILT_IN_DEFAULT;
1367
+ writeJson(PROFILES_FILE, data);
1368
+ debug(`use: wrote ${PROFILES_FILE}`);
1369
+ console.log("Default set to built-in official Anthropic models.");
1370
+ return;
1371
+ }
1372
+ if (!name) {
1373
+ throw new Error("Profile name is required. Use --built-in for official Anthropic models.");
1374
+ }
1237
1375
  if (!data.profiles[name]) {
1238
1376
  throw new Error(`Profile '${name}' not found.`);
1239
1377
  }
@@ -1246,10 +1384,14 @@ function useCommand() {
1246
1384
  }));
1247
1385
  }
1248
1386
  function runCommand() {
1249
- 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(safeAction((args) => {
1387
+ return new Command2("run").description("Launch Claude Code using the default or a specified profile").option("--built-in", "Use official Anthropic models (no custom profile)").allowUnknownOption().argument("[args...]", "Optional profile name followed by extra arguments").action(safeAction((args, opts) => {
1250
1388
  fixJsonFile(CLAUDE_JSON);
1251
1389
  ensureProfilesFile();
1252
1390
  const data = readJson(PROFILES_FILE);
1391
+ if (opts.builtIn) {
1392
+ execClaudeBuiltIn(args);
1393
+ return;
1394
+ }
1253
1395
  let profileName = "";
1254
1396
  let claudeArgs;
1255
1397
  if (args.length > 0 && data.profiles[args[0]]) {
@@ -1259,8 +1401,12 @@ function runCommand() {
1259
1401
  profileName = data.default || "";
1260
1402
  claudeArgs = args;
1261
1403
  }
1404
+ if (profileName === BUILT_IN_DEFAULT) {
1405
+ execClaudeBuiltIn(claudeArgs);
1406
+ return;
1407
+ }
1262
1408
  if (!profileName) {
1263
- throw new Error("No default profile set. Use 'cc-hub use <name>' first.");
1409
+ throw new Error("No default profile set. Use 'cc-hub use <name>' or 'cc-hub use --built-in' first.");
1264
1410
  }
1265
1411
  const p = data.profiles[profileName];
1266
1412
  debug(`run: launching claude with profile '${profileName}', args=[${claudeArgs.join(", ")}]`);
@@ -1480,7 +1626,7 @@ function encodePath(p) {
1480
1626
  debug(`codec: encode "${p}" -> "${encoded}"`);
1481
1627
  return encoded;
1482
1628
  }
1483
- function decodePath(encoded) {
1629
+ function decodePath2(encoded) {
1484
1630
  const decoded = createPathCodec().decode(encoded);
1485
1631
  debug(`codec: decode "${encoded}" -> "${decoded}"`);
1486
1632
  return decoded;
@@ -1596,11 +1742,54 @@ function snippet(text, query, width = 150) {
1596
1742
  const suffix = end < text.length ? "..." : "";
1597
1743
  return prefix + text.slice(start, end) + suffix;
1598
1744
  }
1745
+ function findSessionFile(sessionQuery, projectQuery) {
1746
+ debug(`sessions: findSessionFile query="${sessionQuery}" project=${projectQuery || "(any)"}`);
1747
+ let searchDirs = [];
1748
+ if (projectQuery) {
1749
+ const projDir = findProjectDir(projectQuery);
1750
+ if (!projDir) {
1751
+ throw new Error(`No project matched: ${projectQuery}`);
1752
+ }
1753
+ searchDirs.push(path6.join(PROJECTS_DIR, projDir));
1754
+ } else {
1755
+ try {
1756
+ searchDirs = fs6.readdirSync(PROJECTS_DIR).map((d) => path6.join(PROJECTS_DIR, d));
1757
+ } catch {
1758
+ throw new Error(`No projects directory found at ${PROJECTS_DIR}`);
1759
+ }
1760
+ }
1761
+ const matches = [];
1762
+ for (const dir of searchDirs) {
1763
+ let files;
1764
+ try {
1765
+ files = fs6.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1766
+ } catch {
1767
+ continue;
1768
+ }
1769
+ for (const file of files) {
1770
+ const sessionId = file.replace(/\.jsonl$/, "");
1771
+ if (sessionId.toLowerCase().includes(sessionQuery.toLowerCase())) {
1772
+ matches.push({ filePath: path6.join(dir, file), project: path6.basename(dir) });
1773
+ }
1774
+ }
1775
+ }
1776
+ if (matches.length === 0) {
1777
+ return null;
1778
+ }
1779
+ if (matches.length > 1) {
1780
+ const lines = matches.map((m) => ` ${path6.basename(m.filePath)} in ${decodePath(m.project)}`).join("\n");
1781
+ throw new Error(`Multiple sessions matched '${sessionQuery}':
1782
+ ${lines}
1783
+ Use --project to disambiguate.`);
1784
+ }
1785
+ return matches[0];
1786
+ }
1599
1787
 
1600
1788
  // src/sessions/commands.ts
1601
1789
  import { Command as Command4 } from "commander";
1602
1790
  import fs7 from "fs";
1603
1791
  import path7 from "path";
1792
+ import { spawnSync as spawnSync3 } from "child_process";
1604
1793
  function sessionCommand() {
1605
1794
  const session = new Command4("session").description("Manage Claude Code sessions");
1606
1795
  const desktopApp2 = createDesktopApp();
@@ -1630,7 +1819,7 @@ function sessionCommand() {
1630
1819
  } catch {
1631
1820
  }
1632
1821
  const stat = fs7.statSync(fullPath);
1633
- const decoded = decodePath(projDir);
1822
+ const decoded = decodePath2(projDir);
1634
1823
  if (opts.json) {
1635
1824
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
1636
1825
  } else if (opts.short) {
@@ -1648,7 +1837,7 @@ function sessionCommand() {
1648
1837
  throw new Error(`No project matched: ${project}`);
1649
1838
  }
1650
1839
  const fullPath = path7.join(PROJECTS_DIR, projDir);
1651
- console.log(`Project: ${decodePath(projDir)}`);
1840
+ console.log(`Project: ${decodePath2(projDir)}`);
1652
1841
  console.log(`Dir: ${fullPath}`);
1653
1842
  console.log("");
1654
1843
  const fmt = (sid, name, started, msgs) => `${sid.padEnd(36)} ${name.padEnd(30)} ${started.padEnd(17)} ${msgs}`;
@@ -1742,7 +1931,7 @@ function sessionCommand() {
1742
1931
  const relPath = path7.relative(baseDir, fullPath);
1743
1932
  const projEnc = relPath.split(path7.sep)[0];
1744
1933
  const sessionId = path7.basename(fullPath, ".jsonl");
1745
- const projName = label ? projEnc : decodePath(projEnc);
1934
+ const projName = label ? projEnc : decodePath2(projEnc);
1746
1935
  console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1747
1936
  found = true;
1748
1937
  count++;
@@ -1928,6 +2117,38 @@ function sessionCommand() {
1928
2117
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1929
2118
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
1930
2119
  }));
2120
+ session.command("troubleshoot").description("Launch Claude Code to troubleshoot a session file").argument("<session>", "Session ID or partial match").option("-i, --interactive", "Open an interactive Claude Code window instead of a one-shot prompt").action(safeAction((sessionId, opts) => {
2121
+ debug(`session troubleshoot: session=${sessionId} interactive=${!!opts.interactive}`);
2122
+ console.log(`Searching for session '${sessionId}'...`);
2123
+ const match = findSessionFile(sessionId);
2124
+ if (!match) {
2125
+ throw new Error(`Session '${sessionId}' not found.`);
2126
+ }
2127
+ if (!fs7.existsSync(match.filePath)) {
2128
+ throw new Error(`Session file no longer exists: ${match.filePath}`);
2129
+ }
2130
+ console.log(`Found session file: ${match.filePath}`);
2131
+ const nodeBinary = process.argv[0];
2132
+ const scriptPath = process.argv[1];
2133
+ let args;
2134
+ const promptText = `Please analyze this Claude Code session file: ${match.filePath}
2135
+
2136
+ The file contains a JSONL conversation history. Review it for any errors, anomalies, or issues (truncated responses, failed tool calls, error messages, corrupted data, etc.). Summarize what happened in the session and identify any problems that need attention. If the file is very large, focus on the most recent turns and any lines containing "error", "exception", "failed", or non-JSON content.`;
2137
+ if (opts.interactive) {
2138
+ console.log("Launching Claude Code (interactive)...");
2139
+ info(`session troubleshoot: launching cc-hub run (interactive) for ${match.filePath}`);
2140
+ args = ["run", promptText];
2141
+ } else {
2142
+ console.log("Launching Claude Code with prompt...");
2143
+ info(`session troubleshoot: launching cc-hub run -p "${promptText}"`);
2144
+ args = ["run", "-p", promptText];
2145
+ }
2146
+ const result = spawnSync3(nodeBinary, [scriptPath, ...args], {
2147
+ stdio: "inherit",
2148
+ shell: process.platform === "win32"
2149
+ });
2150
+ process.exit(result.status ?? 1);
2151
+ }));
1931
2152
  return session;
1932
2153
  }
1933
2154
 
@@ -1941,7 +2162,7 @@ _cc-hub() {
1941
2162
  local -a commands
1942
2163
  commands=(
1943
2164
  'profile:Manage Claude CLI profiles'
1944
- 'use:Launch Claude Code with a saved profile'
2165
+ 'use:Set a profile as the default'
1945
2166
  'run:Launch Claude Code using the default or a specified profile'
1946
2167
  'hook:Manage Claude Code hooks in settings.json'
1947
2168
  'session:Manage Claude Code sessions'
@@ -1985,6 +2206,7 @@ _cc-hub() {
1985
2206
  'ps:Show active Claude Code processes'
1986
2207
  'stats:Show summary statistics'
1987
2208
  'clean:Delete session JSONL files older than N days'
2209
+ 'troubleshoot:Launch Claude Code to troubleshoot a session file'
1988
2210
  )
1989
2211
 
1990
2212
  _cc_hub_profiles() {
@@ -2019,8 +2241,10 @@ _cc-hub() {
2019
2241
  profile)
2020
2242
  if (( CURRENT == 2 )); then
2021
2243
  _describe -t profile-subcmds 'profile subcommand' profile_subcmds
2022
- elif [[ $words[2] == "view" || $words[2] == "remove" || $words[2] == "default" ]]; then
2244
+ elif [[ $words[2] == "view" || $words[2] == "remove" ]]; then
2023
2245
  _cc_hub_profiles
2246
+ elif [[ $words[2] == "default" ]]; then
2247
+ _arguments -C -S '--built-in[Use official Anthropic models as default]' '*:profile:_cc_hub_profiles'
2024
2248
  elif [[ $words[2] == "rename" ]]; then
2025
2249
  if (( CURRENT == 3 )); then
2026
2250
  _cc_hub_profiles
@@ -2052,7 +2276,7 @@ _cc-hub() {
2052
2276
  fi
2053
2277
  ;;
2054
2278
  use|run)
2055
- _cc_hub_profiles
2279
+ _arguments -C -S '--built-in[Use official Anthropic models (no custom profile)]' '*:profile:_cc_hub_profiles'
2056
2280
  ;;
2057
2281
  hook)
2058
2282
  if (( CURRENT == 2 )); then
@@ -2062,6 +2286,8 @@ _cc-hub() {
2062
2286
  session)
2063
2287
  if (( CURRENT == 2 )); then
2064
2288
  _describe -t session-subcmds 'session subcommand' session_subcmds
2289
+ elif [[ $words[2] == "troubleshoot" ]]; then
2290
+ _arguments -C -S '(-i --interactive)'{-i,--interactive}'[Open an interactive Claude Code window instead of a one-shot prompt]'
2065
2291
  fi
2066
2292
  ;;
2067
2293
  esac
@@ -2073,20 +2299,22 @@ compdef _cc-hub cc-hub
2073
2299
  `;
2074
2300
 
2075
2301
  // src/complete/bash.ts
2076
- var BASH_COMPLETION = `_cc-hub_profiles() {
2302
+ var BASH_COMPLETION = `_cc-hub_profile_names() {
2077
2303
  local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
2078
2304
  if [[ -f "$profiles_file" ]]; then
2079
- local names
2080
- names=$(command python3 -c "
2305
+ command python3 -c "
2081
2306
  import json
2082
2307
  data = json.load(open('$profiles_file'))
2083
2308
  for name in data.get('profiles', {}):
2084
2309
  print(name)
2085
- " 2>/dev/null)
2086
- COMPREPLY=($(compgen -W "$names" -- "\${cur}"))
2310
+ " 2>/dev/null
2087
2311
  fi
2088
2312
  }
2089
2313
 
2314
+ _cc-hub_profiles() {
2315
+ COMPREPLY=($(compgen -W "$(_cc-hub_profile_names)" -- "\${cur}"))
2316
+ }
2317
+
2090
2318
  _cc-hub_models_for_profile() {
2091
2319
  local profile_name="$1"
2092
2320
  local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
@@ -2121,7 +2349,7 @@ _cc-hub() {
2121
2349
  local provider_subcmds="list"
2122
2350
  local provider_types="anthropic openai"
2123
2351
  local hooks_subcmds="list add remove enable disable"
2124
- local session_subcmds="list show search ps stats clean"
2352
+ local session_subcmds="list show search ps stats clean troubleshoot"
2125
2353
 
2126
2354
  # Top-level command
2127
2355
  if [[ \${COMP_CWORD} -eq 1 ]]; then
@@ -2135,8 +2363,10 @@ _cc-hub() {
2135
2363
  profile)
2136
2364
  if [[ \${COMP_CWORD} -eq 2 ]]; then
2137
2365
  COMPREPLY=($(compgen -W "$profile_subcmds" -- "$cur"))
2138
- elif [[ "$prev" == "view" || "$prev" == "remove" || "$prev" == "default" ]]; then
2366
+ elif [[ "$prev" == "view" || "$prev" == "remove" ]]; then
2139
2367
  _cc-hub_profiles
2368
+ elif [[ "$prev" == "default" ]]; then
2369
+ COMPREPLY=($(compgen -W "--built-in $(_cc-hub_profile_names)" -- "$cur"))
2140
2370
  elif [[ "$prev" == "rename" ]]; then
2141
2371
  _cc-hub_profiles
2142
2372
  elif [[ "$prev" == "profile" ]]; then
@@ -2162,7 +2392,11 @@ _cc-hub() {
2162
2392
  fi
2163
2393
  ;;
2164
2394
  use|run)
2165
- _cc-hub_profiles
2395
+ if [[ "$prev" == "--built-in" ]]; then
2396
+ :
2397
+ else
2398
+ COMPREPLY=($(compgen -W "--built-in $(_cc-hub_profile_names)" -- "$cur"))
2399
+ fi
2166
2400
  ;;
2167
2401
  hook)
2168
2402
  if [[ \${COMP_CWORD} -eq 2 ]]; then
@@ -2172,6 +2406,13 @@ _cc-hub() {
2172
2406
  session)
2173
2407
  if [[ \${COMP_CWORD} -eq 2 ]]; then
2174
2408
  COMPREPLY=($(compgen -W "$session_subcmds" -- "$cur"))
2409
+ elif [[ "\${COMP_WORDS[2]}" == "troubleshoot" ]]; then
2410
+ if [[ "$prev" == "--interactive" || "$prev" == "-i" ]]; then
2411
+ :
2412
+ else
2413
+ local troubleshoot_opts="--interactive -i"
2414
+ COMPREPLY=($(compgen -W "$troubleshoot_opts" -- "$cur"))
2415
+ fi
2175
2416
  fi
2176
2417
  ;;
2177
2418
  esac
@@ -2199,7 +2440,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2199
2440
 
2200
2441
  $profileSubcmds = @('add', 'update', 'list', 'view', 'remove', 'rename', 'default', 'sync')
2201
2442
  $hookSubcmds = @('list', 'add', 'remove', 'enable', 'disable')
2202
- $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean')
2443
+ $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean', 'troubleshoot')
2203
2444
  $providerSubcmds = @('list')
2204
2445
 
2205
2446
  $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
@@ -2217,6 +2458,10 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2217
2458
  $profileSubcmds | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
2218
2459
  return
2219
2460
  }
2461
+ if ($tokens[2] -eq 'default' -and $tokens.Count -ge 3) {
2462
+ $opts = @('--built-in')
2463
+ $opts | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
2464
+ }
2220
2465
  }
2221
2466
  'hook' {
2222
2467
  if ($tokens.Count -eq 2 -or ($tokens.Count -eq 3 -and $wordToComplete -ne '')) {
@@ -2236,6 +2481,16 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2236
2481
  return
2237
2482
  }
2238
2483
  }
2484
+ 'use' {
2485
+ $opts = @('--built-in')
2486
+ $opts | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
2487
+ return
2488
+ }
2489
+ 'run' {
2490
+ $opts = @('--built-in')
2491
+ $opts | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
2492
+ return
2493
+ }
2239
2494
  }
2240
2495
  }`;
2241
2496
 
@@ -2275,6 +2530,7 @@ program.addCommand(hooksCommand());
2275
2530
  program.addCommand(sessionCommand());
2276
2531
  program.addCommand(completionCommand());
2277
2532
  program.addCommand(providerCommand());
2533
+ program.addCommand(proxyCommand());
2278
2534
  try {
2279
2535
  program.parse();
2280
2536
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {