cc-hub-cli 1.1.5 → 1.1.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 +38 -0
  2. package/dist/index.js +105 -11
  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
@@ -558,17 +558,26 @@ function transformAnthropicToOpenAI(body) {
558
558
  });
559
559
  }
560
560
  const contentParts = msg.content.filter(
561
- (b) => b.type === "text" && b.text || b.type === "image" && b.source
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
562
  );
563
563
  if (contentParts.length > 0) {
564
564
  const converted = contentParts.map((part) => {
565
565
  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 } };
566
+ if (part.source?.type === "base64" && part.source.media_type && part.source.data) {
567
+ const url = `data:${part.source.media_type};base64,${part.source.data}`;
568
+ debug(`transform: converting base64 image (${part.source.media_type}, ${part.source.data.length} chars)`);
569
+ return { type: "image_url", image_url: { url } };
570
+ } else if (part.source?.type === "url" && part.source.url) {
571
+ debug(`transform: converting image url (${part.source.url.slice(0, 80)}...)`);
572
+ return { type: "image_url", image_url: { url: part.source.url } };
573
+ }
574
+ warn(`transform: skipping invalid image block (missing source fields)`);
575
+ return null;
568
576
  }
569
577
  return { type: "text", text: part.text };
570
- });
571
- if (converted.every((p) => p.type === "text")) {
578
+ }).filter(Boolean);
579
+ if (converted.length === 0) {
580
+ } else if (converted.every((p) => p.type === "text")) {
572
581
  messages.push({
573
582
  role: "user",
574
583
  content: converted.map((p) => p.text).join("")
@@ -1480,7 +1489,7 @@ function encodePath(p) {
1480
1489
  debug(`codec: encode "${p}" -> "${encoded}"`);
1481
1490
  return encoded;
1482
1491
  }
1483
- function decodePath(encoded) {
1492
+ function decodePath2(encoded) {
1484
1493
  const decoded = createPathCodec().decode(encoded);
1485
1494
  debug(`codec: decode "${encoded}" -> "${decoded}"`);
1486
1495
  return decoded;
@@ -1596,11 +1605,54 @@ function snippet(text, query, width = 150) {
1596
1605
  const suffix = end < text.length ? "..." : "";
1597
1606
  return prefix + text.slice(start, end) + suffix;
1598
1607
  }
1608
+ function findSessionFile(sessionQuery, projectQuery) {
1609
+ debug(`sessions: findSessionFile query="${sessionQuery}" project=${projectQuery || "(any)"}`);
1610
+ let searchDirs = [];
1611
+ if (projectQuery) {
1612
+ const projDir = findProjectDir(projectQuery);
1613
+ if (!projDir) {
1614
+ throw new Error(`No project matched: ${projectQuery}`);
1615
+ }
1616
+ searchDirs.push(path6.join(PROJECTS_DIR, projDir));
1617
+ } else {
1618
+ try {
1619
+ searchDirs = fs6.readdirSync(PROJECTS_DIR).map((d) => path6.join(PROJECTS_DIR, d));
1620
+ } catch {
1621
+ throw new Error(`No projects directory found at ${PROJECTS_DIR}`);
1622
+ }
1623
+ }
1624
+ const matches = [];
1625
+ for (const dir of searchDirs) {
1626
+ let files;
1627
+ try {
1628
+ files = fs6.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1629
+ } catch {
1630
+ continue;
1631
+ }
1632
+ for (const file of files) {
1633
+ const sessionId = file.replace(/\.jsonl$/, "");
1634
+ if (sessionId.toLowerCase().includes(sessionQuery.toLowerCase())) {
1635
+ matches.push({ filePath: path6.join(dir, file), project: path6.basename(dir) });
1636
+ }
1637
+ }
1638
+ }
1639
+ if (matches.length === 0) {
1640
+ return null;
1641
+ }
1642
+ if (matches.length > 1) {
1643
+ const lines = matches.map((m) => ` ${path6.basename(m.filePath)} in ${decodePath(m.project)}`).join("\n");
1644
+ throw new Error(`Multiple sessions matched '${sessionQuery}':
1645
+ ${lines}
1646
+ Use --project to disambiguate.`);
1647
+ }
1648
+ return matches[0];
1649
+ }
1599
1650
 
1600
1651
  // src/sessions/commands.ts
1601
1652
  import { Command as Command4 } from "commander";
1602
1653
  import fs7 from "fs";
1603
1654
  import path7 from "path";
1655
+ import { spawnSync as spawnSync3 } from "child_process";
1604
1656
  function sessionCommand() {
1605
1657
  const session = new Command4("session").description("Manage Claude Code sessions");
1606
1658
  const desktopApp2 = createDesktopApp();
@@ -1630,7 +1682,7 @@ function sessionCommand() {
1630
1682
  } catch {
1631
1683
  }
1632
1684
  const stat = fs7.statSync(fullPath);
1633
- const decoded = decodePath(projDir);
1685
+ const decoded = decodePath2(projDir);
1634
1686
  if (opts.json) {
1635
1687
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
1636
1688
  } else if (opts.short) {
@@ -1648,7 +1700,7 @@ function sessionCommand() {
1648
1700
  throw new Error(`No project matched: ${project}`);
1649
1701
  }
1650
1702
  const fullPath = path7.join(PROJECTS_DIR, projDir);
1651
- console.log(`Project: ${decodePath(projDir)}`);
1703
+ console.log(`Project: ${decodePath2(projDir)}`);
1652
1704
  console.log(`Dir: ${fullPath}`);
1653
1705
  console.log("");
1654
1706
  const fmt = (sid, name, started, msgs) => `${sid.padEnd(36)} ${name.padEnd(30)} ${started.padEnd(17)} ${msgs}`;
@@ -1742,7 +1794,7 @@ function sessionCommand() {
1742
1794
  const relPath = path7.relative(baseDir, fullPath);
1743
1795
  const projEnc = relPath.split(path7.sep)[0];
1744
1796
  const sessionId = path7.basename(fullPath, ".jsonl");
1745
- const projName = label ? projEnc : decodePath(projEnc);
1797
+ const projName = label ? projEnc : decodePath2(projEnc);
1746
1798
  console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1747
1799
  found = true;
1748
1800
  count++;
@@ -1928,6 +1980,38 @@ function sessionCommand() {
1928
1980
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1929
1981
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
1930
1982
  }));
1983
+ 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) => {
1984
+ debug(`session troubleshoot: session=${sessionId} interactive=${!!opts.interactive}`);
1985
+ console.log(`Searching for session '${sessionId}'...`);
1986
+ const match = findSessionFile(sessionId);
1987
+ if (!match) {
1988
+ throw new Error(`Session '${sessionId}' not found.`);
1989
+ }
1990
+ if (!fs7.existsSync(match.filePath)) {
1991
+ throw new Error(`Session file no longer exists: ${match.filePath}`);
1992
+ }
1993
+ console.log(`Found session file: ${match.filePath}`);
1994
+ const nodeBinary = process.argv[0];
1995
+ const scriptPath = process.argv[1];
1996
+ let args;
1997
+ const promptText = `Please analyze this Claude Code session file: ${match.filePath}
1998
+
1999
+ 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.`;
2000
+ if (opts.interactive) {
2001
+ console.log("Launching Claude Code (interactive)...");
2002
+ info(`session troubleshoot: launching cc-hub run (interactive) for ${match.filePath}`);
2003
+ args = ["run", promptText];
2004
+ } else {
2005
+ console.log("Launching Claude Code with prompt...");
2006
+ info(`session troubleshoot: launching cc-hub run -p "${promptText}"`);
2007
+ args = ["run", "-p", promptText];
2008
+ }
2009
+ const result = spawnSync3(nodeBinary, [scriptPath, ...args], {
2010
+ stdio: "inherit",
2011
+ shell: process.platform === "win32"
2012
+ });
2013
+ process.exit(result.status ?? 1);
2014
+ }));
1931
2015
  return session;
1932
2016
  }
1933
2017
 
@@ -1985,6 +2069,7 @@ _cc-hub() {
1985
2069
  'ps:Show active Claude Code processes'
1986
2070
  'stats:Show summary statistics'
1987
2071
  'clean:Delete session JSONL files older than N days'
2072
+ 'troubleshoot:Launch Claude Code to troubleshoot a session file'
1988
2073
  )
1989
2074
 
1990
2075
  _cc_hub_profiles() {
@@ -2062,6 +2147,8 @@ _cc-hub() {
2062
2147
  session)
2063
2148
  if (( CURRENT == 2 )); then
2064
2149
  _describe -t session-subcmds 'session subcommand' session_subcmds
2150
+ elif [[ $words[2] == "troubleshoot" ]]; then
2151
+ _arguments -C -S '(-i --interactive)'{-i,--interactive}'[Open an interactive Claude Code window instead of a one-shot prompt]'
2065
2152
  fi
2066
2153
  ;;
2067
2154
  esac
@@ -2121,7 +2208,7 @@ _cc-hub() {
2121
2208
  local provider_subcmds="list"
2122
2209
  local provider_types="anthropic openai"
2123
2210
  local hooks_subcmds="list add remove enable disable"
2124
- local session_subcmds="list show search ps stats clean"
2211
+ local session_subcmds="list show search ps stats clean troubleshoot"
2125
2212
 
2126
2213
  # Top-level command
2127
2214
  if [[ \${COMP_CWORD} -eq 1 ]]; then
@@ -2172,6 +2259,13 @@ _cc-hub() {
2172
2259
  session)
2173
2260
  if [[ \${COMP_CWORD} -eq 2 ]]; then
2174
2261
  COMPREPLY=($(compgen -W "$session_subcmds" -- "$cur"))
2262
+ elif [[ "\${COMP_WORDS[2]}" == "troubleshoot" ]]; then
2263
+ if [[ "$prev" == "--interactive" || "$prev" == "-i" ]]; then
2264
+ :
2265
+ else
2266
+ local troubleshoot_opts="--interactive -i"
2267
+ COMPREPLY=($(compgen -W "$troubleshoot_opts" -- "$cur"))
2268
+ fi
2175
2269
  fi
2176
2270
  ;;
2177
2271
  esac
@@ -2199,7 +2293,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2199
2293
 
2200
2294
  $profileSubcmds = @('add', 'update', 'list', 'view', 'remove', 'rename', 'default', 'sync')
2201
2295
  $hookSubcmds = @('list', 'add', 'remove', 'enable', 'disable')
2202
- $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean')
2296
+ $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean', 'troubleshoot')
2203
2297
  $providerSubcmds = @('list')
2204
2298
 
2205
2299
  $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
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.6",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {