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.
- package/README.md +38 -0
- package/dist/index.js +105 -11
- 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
|
-
|
|
567
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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: ${
|
|
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 :
|
|
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() }
|