cc-hub-cli 1.1.14 → 1.1.16

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 (2) hide show
  1. package/dist/index.js +268 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { Command as Command2 } from "commander";
10
10
  // src/config.ts
11
11
  import fs4 from "fs";
12
12
  import path4 from "path";
13
- import os3 from "os";
13
+ import os4 from "os";
14
14
 
15
15
  // src/platform/desktop-app.ts
16
16
  import fs2 from "fs";
@@ -266,16 +266,55 @@ function sanitizeToolId(id) {
266
266
  }
267
267
  return sanitized;
268
268
  }
269
+ function buildSystemMessage(system) {
270
+ if (typeof system === "string") {
271
+ return { role: "system", content: system };
272
+ }
273
+ if (Array.isArray(system)) {
274
+ const textBlocks = system.filter((b) => b.type === "text" && b.text);
275
+ const hasCacheControl = textBlocks.some((b) => b.cache_control);
276
+ if (hasCacheControl) {
277
+ const parts = textBlocks.map((b) => {
278
+ const part = { type: "text", text: b.text };
279
+ if (b.cache_control) part.cache_control = b.cache_control;
280
+ return part;
281
+ });
282
+ if (parts.length > 0) return { role: "system", content: parts };
283
+ } else {
284
+ const text = textBlocks.map((b) => b.text).join("\n");
285
+ if (text) return { role: "system", content: text };
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+ function convertAnthropicContentPart(part) {
291
+ let convertedPart = null;
292
+ if (part.type === "image") {
293
+ if (part.source?.type === "base64" && part.source.media_type && part.source.data) {
294
+ const url = `data:${part.source.media_type};base64,${part.source.data}`;
295
+ debug(`transform: converting base64 image (${part.source.media_type}, ${part.source.data.length} chars)`);
296
+ convertedPart = { type: "image_url", image_url: { url } };
297
+ } else if (part.source?.type === "url" && part.source.url) {
298
+ debug(`transform: converting image url (${part.source.url.slice(0, 80)}...)`);
299
+ convertedPart = { type: "image_url", image_url: { url: part.source.url } };
300
+ } else {
301
+ warn(`transform: skipping invalid image block (missing source fields)`);
302
+ return null;
303
+ }
304
+ } else if (part.type === "text") {
305
+ convertedPart = { type: "text", text: part.text };
306
+ }
307
+ if (convertedPart && part.cache_control) {
308
+ convertedPart.cache_control = part.cache_control;
309
+ }
310
+ return convertedPart;
311
+ }
269
312
  function transformAnthropicToOpenAI(body) {
270
313
  debug(`transform: anthropic -> openai model=${body.model} messages=${(body.messages ?? []).length}`);
271
314
  const messages = [];
272
315
  if (body.system) {
273
- if (typeof body.system === "string") {
274
- messages.push({ role: "system", content: body.system });
275
- } else if (Array.isArray(body.system)) {
276
- const text = body.system.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
277
- if (text) messages.push({ role: "system", content: text });
278
- }
316
+ const systemMsg = buildSystemMessage(body.system);
317
+ if (systemMsg) messages.push(systemMsg);
279
318
  }
280
319
  for (const msg of body.messages ?? []) {
281
320
  if (typeof msg.content === "string") {
@@ -301,23 +340,9 @@ function transformAnthropicToOpenAI(body) {
301
340
  (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)
302
341
  );
303
342
  if (contentParts.length > 0) {
304
- const converted = contentParts.map((part) => {
305
- if (part.type === "image") {
306
- if (part.source?.type === "base64" && part.source.media_type && part.source.data) {
307
- const url = `data:${part.source.media_type};base64,${part.source.data}`;
308
- debug(`transform: converting base64 image (${part.source.media_type}, ${part.source.data.length} chars)`);
309
- return { type: "image_url", image_url: { url } };
310
- } else if (part.source?.type === "url" && part.source.url) {
311
- debug(`transform: converting image url (${part.source.url.slice(0, 80)}...)`);
312
- return { type: "image_url", image_url: { url: part.source.url } };
313
- }
314
- warn(`transform: skipping invalid image block (missing source fields)`);
315
- return null;
316
- }
317
- return { type: "text", text: part.text };
318
- }).filter(Boolean);
343
+ const converted = contentParts.map(convertAnthropicContentPart).filter(Boolean);
319
344
  if (converted.length === 0) {
320
- } else if (converted.every((p) => p.type === "text")) {
345
+ } else if (converted.every((p) => p.type === "text") && !converted.some((p) => p.cache_control)) {
321
346
  messages.push({
322
347
  role: "user",
323
348
  content: converted.map((p) => p.text).join("")
@@ -334,6 +359,12 @@ function transformAnthropicToOpenAI(body) {
334
359
  if (textParts.length > 0) {
335
360
  assistantMsg.content = textParts.map((b) => b.text).join("\n");
336
361
  }
362
+ const thinkingParts = msg.content.filter(
363
+ (b) => b.type === "thinking" && b.thinking
364
+ );
365
+ if (thinkingParts.length > 0) {
366
+ assistantMsg.reasoning_content = thinkingParts.map((b) => b.thinking).join("\n");
367
+ }
337
368
  const toolUseParts = msg.content.filter(
338
369
  (b) => b.type === "tool_use" && b.id
339
370
  );
@@ -386,6 +417,13 @@ function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
386
417
  const choice = openaiResponse.choices?.[0];
387
418
  if (!choice) throw new Error("No choices in OpenAI response");
388
419
  const content = [];
420
+ if (choice.message?.reasoning_content) {
421
+ content.push({
422
+ type: "thinking",
423
+ thinking: choice.message.reasoning_content,
424
+ signature: ""
425
+ });
426
+ }
389
427
  if (choice.message?.content) {
390
428
  content.push({ type: "text", text: choice.message.content });
391
429
  }
@@ -422,7 +460,8 @@ function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
422
460
  usage: {
423
461
  input_tokens: (openaiResponse.usage?.prompt_tokens ?? 0) - (openaiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0),
424
462
  output_tokens: openaiResponse.usage?.completion_tokens ?? 0,
425
- cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0
463
+ cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0,
464
+ cache_creation_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cache_creation_tokens ?? 0
426
465
  }
427
466
  };
428
467
  }
@@ -445,7 +484,8 @@ data: ${JSON.stringify(data)}
445
484
  usage: {
446
485
  input_tokens: usage.input_tokens ?? 0,
447
486
  output_tokens: 0,
448
- cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
487
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
488
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0
449
489
  }
450
490
  }
451
491
  });
@@ -454,7 +494,7 @@ data: ${JSON.stringify(data)}
454
494
  yield sse("content_block_start", {
455
495
  type: "content_block_start",
456
496
  index: i,
457
- content_block: block.type === "tool_use" ? { type: "tool_use", id: block.id, name: block.name, input: {} } : { type: "text", text: "" }
497
+ content_block: block.type === "tool_use" ? { type: "tool_use", id: block.id, name: block.name, input: {} } : block.type === "thinking" ? { type: "thinking", thinking: "", signature: block.signature ?? "" } : { type: "text", text: "" }
458
498
  });
459
499
  if (block.type === "text" && block.text) {
460
500
  yield sse("content_block_delta", {
@@ -462,6 +502,12 @@ data: ${JSON.stringify(data)}
462
502
  index: i,
463
503
  delta: { type: "text_delta", text: block.text }
464
504
  });
505
+ } else if (block.type === "thinking" && block.thinking) {
506
+ yield sse("content_block_delta", {
507
+ type: "content_block_delta",
508
+ index: i,
509
+ delta: { type: "thinking_delta", thinking: block.thinking }
510
+ });
465
511
  } else if (block.type === "tool_use" && block.input) {
466
512
  yield sse("content_block_delta", {
467
513
  type: "content_block_delta",
@@ -482,9 +528,183 @@ data: ${JSON.stringify(data)}
482
528
  yield sse("message_stop", { type: "message_stop" });
483
529
  }
484
530
 
531
+ // src/provider/kimi.ts
532
+ function transformAnthropicToKimi(body) {
533
+ debug(`transform: anthropic -> kimi model=${body.model} messages=${(body.messages ?? []).length}`);
534
+ const messages = [];
535
+ debug(`transform: anthropic -> kimi messageCount=${(body.messages ?? []).length}`);
536
+ if (body.system) {
537
+ const systemMsg = buildSystemMessage(body.system);
538
+ if (systemMsg) messages.push(systemMsg);
539
+ }
540
+ for (const msg of body.messages ?? []) {
541
+ if (typeof msg.content === "string") {
542
+ messages.push({ role: msg.role, content: msg.content });
543
+ continue;
544
+ }
545
+ if (!Array.isArray(msg.content)) {
546
+ messages.push({ role: msg.role, content: JSON.stringify(msg.content) });
547
+ continue;
548
+ }
549
+ if (msg.role === "user") {
550
+ const toolResults = msg.content.filter(
551
+ (b) => b.type === "tool_result" && b.tool_use_id
552
+ );
553
+ for (const tr of toolResults) {
554
+ messages.push({
555
+ role: "tool",
556
+ tool_call_id: sanitizeToolId(tr.tool_use_id),
557
+ 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)
558
+ });
559
+ }
560
+ const contentParts = msg.content.filter(
561
+ (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
+ );
563
+ if (contentParts.length > 0) {
564
+ const converted = contentParts.map(convertAnthropicContentPart).filter(Boolean);
565
+ if (converted.length === 0) {
566
+ } else if (converted.every((p) => p.type === "text") && !converted.some((p) => p.cache_control)) {
567
+ messages.push({
568
+ role: "user",
569
+ content: converted.map((p) => p.text).join("")
570
+ });
571
+ } else {
572
+ messages.push({ role: "user", content: converted });
573
+ }
574
+ }
575
+ } else if (msg.role === "assistant") {
576
+ const assistantMsg = { role: "assistant", content: null };
577
+ const textParts = msg.content.filter(
578
+ (b) => b.type === "text" && b.text
579
+ );
580
+ if (textParts.length > 0) {
581
+ assistantMsg.content = textParts.map((b) => b.text).join("\n");
582
+ }
583
+ const thinkingParts = msg.content.filter(
584
+ (b) => b.type === "thinking" && b.thinking
585
+ );
586
+ const toolUseParts = msg.content.filter(
587
+ (b) => b.type === "tool_use" && b.id
588
+ );
589
+ if (toolUseParts.length > 0) {
590
+ assistantMsg.tool_calls = toolUseParts.map((b) => ({
591
+ id: sanitizeToolId(b.id),
592
+ type: "function",
593
+ function: {
594
+ name: b.name,
595
+ arguments: JSON.stringify(b.input ?? {})
596
+ }
597
+ }));
598
+ if (thinkingParts.length > 0) {
599
+ assistantMsg.reasoning_content = thinkingParts.map((b) => b.thinking).join("\n");
600
+ } else {
601
+ assistantMsg.reasoning_content = " ";
602
+ }
603
+ } else if (thinkingParts.length > 0) {
604
+ assistantMsg.reasoning_content = thinkingParts.map((b) => b.thinking).join("\n");
605
+ }
606
+ messages.push(assistantMsg);
607
+ }
608
+ }
609
+ const result = {
610
+ model: body.model,
611
+ messages,
612
+ stream: body.stream ?? false
613
+ };
614
+ if (body.max_tokens != null) result.max_tokens = body.max_tokens;
615
+ if (body.temperature != null) result.temperature = body.temperature;
616
+ if (body.tools?.length) {
617
+ debug(`transform: mapping ${body.tools.length} tool(s)`);
618
+ result.tools = body.tools.map((t) => ({
619
+ type: "function",
620
+ function: {
621
+ name: t.name,
622
+ description: t.description ?? "",
623
+ parameters: t.input_schema
624
+ }
625
+ }));
626
+ if (body.tool_choice) {
627
+ const tc = body.tool_choice;
628
+ if (tc.type === "auto" || tc.type === "none" || tc.type === "required") {
629
+ result.tool_choice = tc.type;
630
+ } else if (tc.type === "tool") {
631
+ result.tool_choice = {
632
+ type: "function",
633
+ function: { name: tc.name }
634
+ };
635
+ }
636
+ }
637
+ }
638
+ return result;
639
+ }
640
+ function transformKimiResponseToAnthropic(kimiResponse, originalModel) {
641
+ debug(`transform: kimi -> anthropic model=${kimiResponse.model ?? originalModel} choices=${kimiResponse.choices?.length ?? 0}`);
642
+ const choice = kimiResponse.choices?.[0];
643
+ if (!choice) throw new Error("No choices in Kimi response");
644
+ const content = [];
645
+ if (choice.message?.reasoning_content) {
646
+ content.push({
647
+ type: "thinking",
648
+ thinking: choice.message.reasoning_content,
649
+ signature: ""
650
+ });
651
+ }
652
+ if (choice.message?.content) {
653
+ content.push({ type: "text", text: choice.message.content });
654
+ }
655
+ if (choice.message?.tool_calls?.length) {
656
+ for (const tc of choice.message.tool_calls) {
657
+ let input = {};
658
+ try {
659
+ input = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
660
+ } catch {
661
+ input = { text: tc.function.arguments ?? "" };
662
+ }
663
+ content.push({
664
+ type: "tool_use",
665
+ id: tc.id,
666
+ name: tc.function.name,
667
+ input
668
+ });
669
+ }
670
+ }
671
+ const finishMap = {
672
+ stop: "end_turn",
673
+ length: "max_tokens",
674
+ tool_calls: "tool_use",
675
+ content_filter: "stop_sequence"
676
+ };
677
+ return {
678
+ id: kimiResponse.id ?? `msg_${Date.now()}`,
679
+ type: "message",
680
+ role: "assistant",
681
+ model: originalModel,
682
+ content,
683
+ stop_reason: finishMap[choice.finish_reason] ?? "end_turn",
684
+ stop_sequence: null,
685
+ usage: {
686
+ input_tokens: (kimiResponse.usage?.prompt_tokens ?? 0) - (kimiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0),
687
+ output_tokens: kimiResponse.usage?.completion_tokens ?? 0,
688
+ cache_read_input_tokens: kimiResponse.usage?.prompt_tokens_details?.cached_tokens ?? 0,
689
+ cache_creation_input_tokens: kimiResponse.usage?.prompt_tokens_details?.cache_creation_tokens ?? 0
690
+ }
691
+ };
692
+ }
693
+
485
694
  // src/provider/server.ts
486
695
  import http from "http";
487
- async function startOpenAIProxy(targetUrl, apiKey, model, models = [], modelMappings = {}) {
696
+ import os3 from "os";
697
+ function getKimiHeaders() {
698
+ const platform = os3.platform();
699
+ const stainlessOS = platform === "darwin" ? "MacOS" : platform === "win32" ? "Windows" : "Linux";
700
+ return {
701
+ "X-Stainless-OS": stainlessOS,
702
+ "X-Stainless-Package-Version": "1.1.16",
703
+ "X-Stainless-Runtime": "node",
704
+ "User-Agent": "claude-code/1.0"
705
+ };
706
+ }
707
+ async function startOpenAIProxy(targetUrl, apiKey, model, models = [], modelMappings = {}, provider = "openai") {
488
708
  const base = targetUrl.replace(/\/+$/, "");
489
709
  const server = http.createServer(async (req, res) => {
490
710
  debug(`Proxy request: ${req.method} ${req.url}`);
@@ -512,7 +732,8 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = [], modelMapp
512
732
  const isStream = !!parsed.stream;
513
733
  const requestModel = parsed.model ?? model;
514
734
  const actualModel = modelMappings[requestModel] || requestModel;
515
- const openaiBody = transformAnthropicToOpenAI({ ...parsed, model: actualModel, stream: false });
735
+ const transformBody = provider === "kimi" ? transformAnthropicToKimi : transformAnthropicToOpenAI;
736
+ const openaiBody = transformBody({ ...parsed, model: actualModel, stream: false });
516
737
  if (isStream) {
517
738
  res.writeHead(200, {
518
739
  "Content-Type": "text/event-stream",
@@ -525,7 +746,8 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = [], modelMapp
525
746
  method: "POST",
526
747
  headers: {
527
748
  "Content-Type": "application/json",
528
- "Authorization": `Bearer ${apiKey}`
749
+ "Authorization": `Bearer ${apiKey}`,
750
+ ...provider === "kimi" ? getKimiHeaders() : {}
529
751
  },
530
752
  body: JSON.stringify(openaiBody)
531
753
  });
@@ -540,7 +762,8 @@ data: ${errText}
540
762
  return;
541
763
  }
542
764
  const data2 = await upstream2.json();
543
- const anthropicResponse2 = transformOpenAIResponseToAnthropic(data2, parsed.model ?? model);
765
+ const transformResponse2 = provider === "kimi" ? transformKimiResponseToAnthropic : transformOpenAIResponseToAnthropic;
766
+ const anthropicResponse2 = transformResponse2(data2, parsed.model ?? model);
544
767
  for (const chunk of synthesizeAnthropicSSE(anthropicResponse2)) {
545
768
  res.write(chunk);
546
769
  }
@@ -554,7 +777,8 @@ data: ${errText}
554
777
  method: "POST",
555
778
  headers: {
556
779
  "Content-Type": "application/json",
557
- "Authorization": `Bearer ${apiKey}`
780
+ "Authorization": `Bearer ${apiKey}`,
781
+ ...provider === "kimi" ? getKimiHeaders() : {}
558
782
  },
559
783
  body: JSON.stringify(openaiBody)
560
784
  });
@@ -566,7 +790,8 @@ data: ${errText}
566
790
  return;
567
791
  }
568
792
  const data = await upstream.json();
569
- const anthropicResponse = transformOpenAIResponseToAnthropic(data, parsed.model ?? model);
793
+ const transformResponse = provider === "kimi" ? transformKimiResponseToAnthropic : transformOpenAIResponseToAnthropic;
794
+ const anthropicResponse = transformResponse(data, parsed.model ?? model);
570
795
  res.writeHead(200, { "Content-Type": "application/json" });
571
796
  res.end(JSON.stringify(anthropicResponse));
572
797
  return;
@@ -615,6 +840,10 @@ var PROVIDERS = [
615
840
  {
616
841
  name: "openai",
617
842
  description: "Embedded proxy \u2014 translates Anthropic requests to OpenAI Chat Completions format"
843
+ },
844
+ {
845
+ name: "kimi",
846
+ description: "Embedded proxy \u2014 translates Anthropic requests to Kimi API format (handles reasoning_content compatibility)"
618
847
  }
619
848
  ];
620
849
  var ANTHROPIC_ALIASES = ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"];
@@ -831,10 +1060,10 @@ function createPathCodec() {
831
1060
  }
832
1061
 
833
1062
  // src/config.ts
834
- var CLAUDE_DIR = process.env.CLAUDE_DIR || path4.join(os3.homedir(), ".claude");
1063
+ var CLAUDE_DIR = process.env.CLAUDE_DIR || path4.join(os4.homedir(), ".claude");
835
1064
  var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path4.join(CLAUDE_DIR, "profiles.json");
836
1065
  var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path4.join(CLAUDE_DIR, "settings.json");
837
- var CLAUDE_JSON = path4.join(os3.homedir(), ".claude.json");
1066
+ var CLAUDE_JSON = path4.join(os4.homedir(), ".claude.json");
838
1067
  var PROJECTS_DIR = path4.join(CLAUDE_DIR, "projects");
839
1068
  var SESSIONS_DIR = path4.join(CLAUDE_DIR, "sessions");
840
1069
  var desktopApp = createDesktopApp();
@@ -996,15 +1225,16 @@ function execClaude(profileName, p, extraArgs) {
996
1225
  }
997
1226
  delete env.ANTHROPIC_API_KEY;
998
1227
  info(`Launching Claude with profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"} binary=${binary}`);
999
- if (p.provider === "openai") {
1228
+ if (p.provider === "openai" || p.provider === "kimi") {
1000
1229
  const allModels = p.models || (p.model ? [p.model] : []);
1001
- debug(`execClaude: starting OpenAI proxy for ${allModels.length} model(s)`);
1230
+ debug(`execClaude: starting ${p.provider} proxy for ${allModels.length} model(s)`);
1002
1231
  startOpenAIProxy(
1003
1232
  p.url || "https://api.openai.com",
1004
1233
  p.token || "",
1005
1234
  firstModel || "gpt-4o",
1006
1235
  allModels,
1007
- {}
1236
+ {},
1237
+ p.provider
1008
1238
  ).then(({ baseUrl, stop }) => {
1009
1239
  env.ANTHROPIC_BASE_URL = baseUrl;
1010
1240
  debug(`execClaude: proxy running at ${baseUrl}`);
@@ -1311,7 +1541,7 @@ function profileCommand() {
1311
1541
  ensureSettingsFile();
1312
1542
  const settings2 = readJson(SETTINGS_FILE);
1313
1543
  const exported = Object.fromEntries(
1314
- Object.entries(settings2).filter(([key]) => !key.startsWith("_"))
1544
+ Object.entries(settings2).filter(([key]) => !key.startsWith("_") && key != "env")
1315
1545
  );
1316
1546
  const env = {
1317
1547
  ...typeof exported.env === "object" && exported.env !== null ? exported.env : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {