cc-hub-cli 1.1.4 → 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 +467 -213
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8,14 +8,110 @@ import { createRequire } from "module";
8
8
  import { Command as Command2 } from "commander";
9
9
 
10
10
  // src/config.ts
11
- import fs3 from "fs";
12
- import path3 from "path";
13
- import os2 from "os";
11
+ import fs4 from "fs";
12
+ import path4 from "path";
13
+ import os3 from "os";
14
14
 
15
15
  // src/platform/desktop-app.ts
16
+ import fs2 from "fs";
17
+ import path2 from "path";
18
+ import os2 from "os";
19
+
20
+ // src/logger.ts
16
21
  import fs from "fs";
17
22
  import path from "path";
18
23
  import os from "os";
24
+ var LOG_DIR = path.join(os.homedir(), ".claude", "cc-hub", "logs");
25
+ var LEVEL_PRIORITY = {
26
+ DEBUG: 0,
27
+ INFO: 1,
28
+ WARN: 2,
29
+ ERROR: 3
30
+ };
31
+ var currentLevel = "INFO";
32
+ function setLogLevel(level) {
33
+ const upper = level.toUpperCase();
34
+ if (upper in LEVEL_PRIORITY) {
35
+ currentLevel = upper;
36
+ }
37
+ }
38
+ function shouldLog(level) {
39
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
40
+ }
41
+ function ensureLogDir() {
42
+ if (!fs.existsSync(LOG_DIR)) {
43
+ fs.mkdirSync(LOG_DIR, { recursive: true });
44
+ }
45
+ }
46
+ function logFilePath() {
47
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
48
+ return path.join(LOG_DIR, `cc-hub-${date}.log`);
49
+ }
50
+ function formatLine(level, message) {
51
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
52
+ return `[${ts}] [${level}] ${message}
53
+ `;
54
+ }
55
+ function write(level, message, err) {
56
+ if (!shouldLog(level)) return;
57
+ ensureLogDir();
58
+ let line = formatLine(level, message);
59
+ if (err instanceof Error) {
60
+ line += formatLine(level, err.stack || err.message);
61
+ } else if (err !== void 0) {
62
+ line += formatLine(level, String(err));
63
+ }
64
+ try {
65
+ fs.appendFileSync(logFilePath(), line, "utf-8");
66
+ } catch {
67
+ }
68
+ }
69
+ function error(message, err) {
70
+ write("ERROR", message, err);
71
+ }
72
+ function warn(message, err) {
73
+ write("WARN", message, err);
74
+ }
75
+ function info(message) {
76
+ write("INFO", message);
77
+ }
78
+ function debug(message) {
79
+ write("DEBUG", message);
80
+ }
81
+ function installGlobalExceptionHandlers() {
82
+ process.on("uncaughtException", (err) => {
83
+ error("Uncaught exception", err);
84
+ console.error("Unexpected error:", err.message);
85
+ process.exit(1);
86
+ });
87
+ process.on("unhandledRejection", (reason) => {
88
+ error("Unhandled rejection", reason instanceof Error ? reason : new Error(String(reason)));
89
+ console.error("Unhandled promise rejection:", reason);
90
+ process.exit(1);
91
+ });
92
+ }
93
+ function safeAction(fn) {
94
+ return ((...args) => {
95
+ info(`Executing: ${process.argv.slice(2).join(" ")}`);
96
+ try {
97
+ const result = fn(...args);
98
+ if (result && typeof result.then === "function") {
99
+ return result.catch((err) => {
100
+ error(`Command failed: ${err instanceof Error ? err.message : String(err)}`, err);
101
+ console.error("Error:", err instanceof Error ? err.message : String(err));
102
+ process.exit(1);
103
+ });
104
+ }
105
+ return result;
106
+ } catch (err) {
107
+ error(`Command failed: ${err instanceof Error ? err.message : String(err)}`, err);
108
+ console.error("Error:", err instanceof Error ? err.message : String(err));
109
+ process.exit(1);
110
+ }
111
+ });
112
+ }
113
+
114
+ // src/platform/desktop-app.ts
19
115
  function sortSemverDesc(a, b) {
20
116
  const parse = (v) => v.split(".").map((n) => parseInt(n, 10));
21
117
  const av = parse(a);
@@ -28,58 +124,61 @@ function sortSemverDesc(a, b) {
28
124
  return 0;
29
125
  }
30
126
  var MacOSDesktopApp = class {
31
- supportDir = path.join(os.homedir(), "Library/Application Support/Claude-3p");
127
+ supportDir = path2.join(os2.homedir(), "Library/Application Support/Claude-3p");
32
128
  isInstalled() {
33
- return fs.existsSync(this.supportDir);
129
+ return fs2.existsSync(this.supportDir);
34
130
  }
35
131
  getSupportDir() {
36
132
  return this.isInstalled() ? this.supportDir : void 0;
37
133
  }
38
134
  getSessionsDir() {
39
- return this.isInstalled() ? path.join(this.supportDir, "local-agent-mode-sessions") : void 0;
135
+ return this.isInstalled() ? path2.join(this.supportDir, "local-agent-mode-sessions") : void 0;
40
136
  }
41
137
  getConfigLibrary() {
42
138
  if (!this.isInstalled()) return void 0;
43
- const configLib = path.join(this.supportDir, "configLibrary");
44
- if (fs.existsSync(path.join(configLib, "_meta.json"))) return configLib;
45
- if (fs.existsSync(configLib)) return configLib;
139
+ const configLib = path2.join(this.supportDir, "configLibrary");
140
+ if (fs2.existsSync(path2.join(configLib, "_meta.json"))) return configLib;
141
+ if (fs2.existsSync(configLib)) return configLib;
46
142
  return configLib;
47
143
  }
48
144
  findBinary() {
49
- const claudeCodeDir = path.join(this.supportDir, "claude-code");
50
- if (!fs.existsSync(claudeCodeDir)) return void 0;
145
+ debug(`desktop-app: searching for binary in ${this.supportDir}`);
146
+ const claudeCodeDir = path2.join(this.supportDir, "claude-code");
147
+ if (!fs2.existsSync(claudeCodeDir)) return void 0;
51
148
  let versions;
52
149
  try {
53
- versions = fs.readdirSync(claudeCodeDir).filter(
54
- (d) => fs.existsSync(path.join(claudeCodeDir, d, "claude.app", "Contents", "MacOS", "claude"))
150
+ versions = fs2.readdirSync(claudeCodeDir).filter(
151
+ (d) => fs2.existsSync(path2.join(claudeCodeDir, d, "claude.app", "Contents", "MacOS", "claude"))
55
152
  );
56
153
  } catch {
57
154
  return void 0;
58
155
  }
59
156
  if (versions.length === 0) return void 0;
60
157
  versions.sort(sortSemverDesc);
61
- return path.join(claudeCodeDir, versions[0], "claude.app", "Contents", "MacOS", "claude");
158
+ const binary = path2.join(claudeCodeDir, versions[0], "claude.app", "Contents", "MacOS", "claude");
159
+ debug(`desktop-app: found macOS binary ${binary}`);
160
+ return binary;
62
161
  }
63
162
  };
64
163
  var WindowsDesktopApp = class {
65
164
  _buildCandidates() {
66
165
  const candidates = [
67
- path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude-3p"),
68
- path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude"),
69
- path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Claude-3p"),
70
- path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Claude")
166
+ path2.join(process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming"), "Claude-3p"),
167
+ path2.join(process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming"), "Claude"),
168
+ path2.join(process.env.LOCALAPPDATA || path2.join(os2.homedir(), "AppData", "Local"), "Claude-3p"),
169
+ path2.join(process.env.LOCALAPPDATA || path2.join(os2.homedir(), "AppData", "Local"), "Claude")
71
170
  ];
72
- const packagesDir = path.join(
73
- process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
171
+ const packagesDir = path2.join(
172
+ process.env.LOCALAPPDATA || path2.join(os2.homedir(), "AppData", "Local"),
74
173
  "Packages"
75
174
  );
76
- if (fs.existsSync(packagesDir)) {
175
+ if (fs2.existsSync(packagesDir)) {
77
176
  try {
78
- const entries = fs.readdirSync(packagesDir);
177
+ const entries = fs2.readdirSync(packagesDir);
79
178
  for (const entry of entries) {
80
179
  if (entry.startsWith("Claude_")) {
81
- candidates.push(path.join(packagesDir, entry, "LocalCache", "Roaming", "Claude-3p"));
82
- candidates.push(path.join(packagesDir, entry, "LocalCache", "Roaming", "Claude"));
180
+ candidates.push(path2.join(packagesDir, entry, "LocalCache", "Roaming", "Claude-3p"));
181
+ candidates.push(path2.join(packagesDir, entry, "LocalCache", "Roaming", "Claude"));
83
182
  }
84
183
  }
85
184
  } catch {
@@ -89,7 +188,7 @@ var WindowsDesktopApp = class {
89
188
  }
90
189
  _findSupportDir() {
91
190
  for (const dir of this._buildCandidates()) {
92
- if (fs.existsSync(dir)) return dir;
191
+ if (fs2.existsSync(dir)) return dir;
93
192
  }
94
193
  return void 0;
95
194
  }
@@ -102,34 +201,34 @@ var WindowsDesktopApp = class {
102
201
  getConfigLibrary() {
103
202
  const candidates = this._buildCandidates();
104
203
  for (const dir2 of candidates) {
105
- const configLib = path.join(dir2, "configLibrary");
106
- if (fs.existsSync(path.join(configLib, "_meta.json"))) {
204
+ const configLib = path2.join(dir2, "configLibrary");
205
+ if (fs2.existsSync(path2.join(configLib, "_meta.json"))) {
107
206
  return configLib;
108
207
  }
109
208
  }
110
209
  for (const dir2 of candidates) {
111
- const configLib = path.join(dir2, "configLibrary");
112
- if (fs.existsSync(configLib)) {
210
+ const configLib = path2.join(dir2, "configLibrary");
211
+ if (fs2.existsSync(configLib)) {
113
212
  return configLib;
114
213
  }
115
214
  }
116
215
  const dir = this._findSupportDir();
117
- return dir ? path.join(dir, "configLibrary") : void 0;
216
+ return dir ? path2.join(dir, "configLibrary") : void 0;
118
217
  }
119
218
  getSessionsDir() {
120
219
  const candidates = this._buildCandidates();
121
220
  for (const dir2 of candidates) {
122
- const sessionsDir = path.join(dir2, "local-agent-mode-sessions");
123
- if (fs.existsSync(sessionsDir)) {
221
+ const sessionsDir = path2.join(dir2, "local-agent-mode-sessions");
222
+ if (fs2.existsSync(sessionsDir)) {
124
223
  return sessionsDir;
125
224
  }
126
225
  }
127
226
  const dir = this._findSupportDir();
128
- return dir ? path.join(dir, "local-agent-mode-sessions") : void 0;
227
+ return dir ? path2.join(dir, "local-agent-mode-sessions") : void 0;
129
228
  }
130
229
  findBinary() {
131
- const win32Binary = path.join(process.env.LOCALAPPDATA || "", "Programs", "Claude", "Claude.exe");
132
- if (fs.existsSync(win32Binary)) return win32Binary;
230
+ const win32Binary = path2.join(process.env.LOCALAPPDATA || "", "Programs", "Claude", "Claude.exe");
231
+ if (fs2.existsSync(win32Binary)) return win32Binary;
133
232
  return void 0;
134
233
  }
135
234
  };
@@ -152,8 +251,8 @@ var NoOpDesktopApp = class {
152
251
  };
153
252
 
154
253
  // src/platform/profile-syncer.ts
155
- import fs2 from "fs";
156
- import path2 from "path";
254
+ import fs3 from "fs";
255
+ import path3 from "path";
157
256
  import { randomUUID } from "crypto";
158
257
  function toDesktopProfile(p) {
159
258
  const models = p.models || (p.model ? [p.model] : []);
@@ -182,9 +281,10 @@ var DesktopProfileSyncer = class {
182
281
  }
183
282
  sync(name, p) {
184
283
  const configLib = this.app.getConfigLibrary();
284
+ debug(`profile-syncer: sync '${name}' to ${configLib || "(none)"}`);
185
285
  if (!configLib) return;
186
- if (!fs2.existsSync(configLib)) {
187
- fs2.mkdirSync(configLib, { recursive: true });
286
+ if (!fs3.existsSync(configLib)) {
287
+ fs3.mkdirSync(configLib, { recursive: true });
188
288
  }
189
289
  const meta = this.readMeta();
190
290
  const entries = meta.entries || [];
@@ -207,9 +307,11 @@ var DesktopProfileSyncer = class {
207
307
  meta.entries = entries;
208
308
  this.writeMeta(meta);
209
309
  this.writeProfile(id, configLib, toDesktopProfile(p));
310
+ debug(`profile-syncer: synced '${name}' id=${id}`);
210
311
  }
211
312
  remove(name, p) {
212
313
  const configLib = this.app.getConfigLibrary();
314
+ debug(`profile-syncer: remove '${name}' id=${p.desktopId || "(none)"} from ${configLib || "(none)"}`);
213
315
  if (!configLib || !p.desktopId) return;
214
316
  const meta = this.readMeta();
215
317
  if (meta.entries) {
@@ -219,13 +321,14 @@ var DesktopProfileSyncer = class {
219
321
  delete meta.appliedId;
220
322
  }
221
323
  this.writeMeta(meta);
222
- const filePath = path2.join(configLib, `${p.desktopId}.json`);
223
- if (fs2.existsSync(filePath)) {
224
- fs2.unlinkSync(filePath);
324
+ const filePath = path3.join(configLib, `${p.desktopId}.json`);
325
+ if (fs3.existsSync(filePath)) {
326
+ fs3.unlinkSync(filePath);
225
327
  }
226
328
  }
227
329
  setActive(p) {
228
330
  const configLib = this.app.getConfigLibrary();
331
+ debug(`profile-syncer: setActive id=${p.desktopId || "(none)"} in ${configLib || "(none)"}`);
229
332
  if (!configLib || !p.desktopId) return;
230
333
  const meta = this.readMeta();
231
334
  meta.appliedId = p.desktopId;
@@ -238,11 +341,11 @@ var DesktopProfileSyncer = class {
238
341
  }
239
342
  metaFile() {
240
343
  const configLib = this.app.getConfigLibrary();
241
- return configLib ? path2.join(configLib, "_meta.json") : void 0;
344
+ return configLib ? path3.join(configLib, "_meta.json") : void 0;
242
345
  }
243
346
  readMeta() {
244
347
  const file = this.metaFile();
245
- if (!file || !fs2.existsSync(file)) return {};
348
+ if (!file || !fs3.existsSync(file)) return {};
246
349
  try {
247
350
  return readJson(file);
248
351
  } catch {
@@ -254,7 +357,7 @@ var DesktopProfileSyncer = class {
254
357
  if (file) writeJson(file, meta);
255
358
  }
256
359
  writeProfile(id, configLib, data) {
257
- writeJson(path2.join(configLib, `${id}.json`), data);
360
+ writeJson(path3.join(configLib, `${id}.json`), data);
258
361
  }
259
362
  };
260
363
 
@@ -266,21 +369,25 @@ var SystemBinaryResolver = class {
266
369
  }
267
370
  app;
268
371
  resolve() {
372
+ debug("binary-resolver: trying global 'claude' command");
269
373
  try {
270
374
  const result = spawnSync("claude", ["--version"], {
271
375
  shell: process.platform === "win32",
272
376
  encoding: "utf-8"
273
377
  });
274
378
  if (result.status === 0) {
379
+ debug("binary-resolver: found global 'claude'");
275
380
  return "claude";
276
381
  }
277
382
  } catch {
278
383
  }
384
+ debug("binary-resolver: trying desktop app binary");
279
385
  const desktopBinary = this.app.findBinary();
280
- if (desktopBinary) return desktopBinary;
281
- console.error("Error: Could not find Claude Code CLI.");
282
- console.error("Install it globally or install the Claude Code desktop app.");
283
- process.exit(1);
386
+ if (desktopBinary) {
387
+ debug(`binary-resolver: found desktop binary at ${desktopBinary}`);
388
+ return desktopBinary;
389
+ }
390
+ throw new Error("Could not find Claude Code CLI. Install it globally or install the Claude Code desktop app.");
284
391
  }
285
392
  };
286
393
 
@@ -324,30 +431,33 @@ function createPathCodec() {
324
431
  }
325
432
 
326
433
  // src/config.ts
327
- var CLAUDE_DIR = process.env.CLAUDE_DIR || path3.join(os2.homedir(), ".claude");
328
- var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path3.join(CLAUDE_DIR, "profiles.json");
329
- var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path3.join(CLAUDE_DIR, "settings.json");
330
- var CLAUDE_JSON = path3.join(os2.homedir(), ".claude.json");
331
- var PROJECTS_DIR = path3.join(CLAUDE_DIR, "projects");
332
- var SESSIONS_DIR = path3.join(CLAUDE_DIR, "sessions");
434
+ var CLAUDE_DIR = process.env.CLAUDE_DIR || path4.join(os3.homedir(), ".claude");
435
+ var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path4.join(CLAUDE_DIR, "profiles.json");
436
+ var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path4.join(CLAUDE_DIR, "settings.json");
437
+ var CLAUDE_JSON = path4.join(os3.homedir(), ".claude.json");
438
+ var PROJECTS_DIR = path4.join(CLAUDE_DIR, "projects");
439
+ var SESSIONS_DIR = path4.join(CLAUDE_DIR, "sessions");
333
440
  var desktopApp = createDesktopApp();
334
441
  var DESKTOP_CONFIG_LIBRARY = desktopApp.getConfigLibrary() || "";
335
- var DESKTOP_META_FILE = DESKTOP_CONFIG_LIBRARY ? path3.join(DESKTOP_CONFIG_LIBRARY, "_meta.json") : "";
442
+ var DESKTOP_META_FILE = DESKTOP_CONFIG_LIBRARY ? path4.join(DESKTOP_CONFIG_LIBRARY, "_meta.json") : "";
336
443
  var DESKTOP_SESSIONS_DIR = desktopApp.getSessionsDir() || "";
337
444
  function isDesktopAppInstalled() {
338
445
  return desktopApp.isInstalled();
339
446
  }
340
447
  function ensureFile(filePath, defaultContent) {
341
- if (!fs3.existsSync(filePath)) {
342
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
343
- fs3.writeFileSync(filePath, defaultContent, "utf-8");
448
+ if (!fs4.existsSync(filePath)) {
449
+ debug(`ensureFile: creating ${filePath}`);
450
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
451
+ fs4.writeFileSync(filePath, defaultContent, "utf-8");
344
452
  }
345
453
  }
346
454
  function readJson(filePath) {
347
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
455
+ debug(`readJson: ${filePath}`);
456
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
348
457
  }
349
458
  function writeJson(filePath, data) {
350
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
459
+ debug(`writeJson: ${filePath}`);
460
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
351
461
  }
352
462
  function ensureProfilesFile() {
353
463
  ensureFile(PROFILES_FILE, '{"profiles":{}}\n');
@@ -356,12 +466,12 @@ function ensureSettingsFile() {
356
466
  ensureFile(SETTINGS_FILE, "{}\n");
357
467
  }
358
468
  function fixJsonFile(filePath, fallback = {}) {
359
- if (!fs3.existsSync(filePath)) return;
360
- const backupPath = path3.join(CLAUDE_DIR, path3.basename(filePath) + ".backup");
361
- const raw = fs3.readFileSync(filePath, "utf-8");
469
+ if (!fs4.existsSync(filePath)) return;
470
+ const backupPath = path4.join(CLAUDE_DIR, path4.basename(filePath) + ".backup");
471
+ const raw = fs4.readFileSync(filePath, "utf-8");
362
472
  try {
363
473
  JSON.parse(raw);
364
- fs3.copyFileSync(filePath, backupPath);
474
+ fs4.copyFileSync(filePath, backupPath);
365
475
  return;
366
476
  } catch {
367
477
  }
@@ -386,15 +496,18 @@ function fixJsonFile(filePath, fallback = {}) {
386
496
  if (openCurly > 0) text += "}".repeat(openCurly);
387
497
  try {
388
498
  JSON.parse(text);
389
- fs3.writeFileSync(filePath, text + "\n", "utf-8");
390
- console.error(`Fixed invalid JSON in ${path3.basename(filePath)}.`);
499
+ fs4.writeFileSync(filePath, text + "\n", "utf-8");
500
+ warn(`Fixed invalid JSON in ${path4.basename(filePath)}.`);
501
+ console.error(`Fixed invalid JSON in ${path4.basename(filePath)}.`);
391
502
  } catch {
392
- if (fs3.existsSync(backupPath)) {
393
- fs3.copyFileSync(backupPath, filePath);
394
- console.error(`Restored ${path3.basename(filePath)} from backup.`);
503
+ 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.`);
395
507
  } else {
396
508
  writeJson(filePath, fallback);
397
- console.error(`Could not fix ${path3.basename(filePath)}, no backup found, reset to default.`);
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.`);
398
511
  }
399
512
  }
400
513
  }
@@ -414,6 +527,7 @@ function sanitizeToolId(id) {
414
527
  return sanitized;
415
528
  }
416
529
  function transformAnthropicToOpenAI(body) {
530
+ debug(`transform: anthropic -> openai model=${body.model} messages=${(body.messages ?? []).length}`);
417
531
  const messages = [];
418
532
  if (body.system) {
419
533
  if (typeof body.system === "string") {
@@ -444,17 +558,26 @@ function transformAnthropicToOpenAI(body) {
444
558
  });
445
559
  }
446
560
  const contentParts = msg.content.filter(
447
- (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)
448
562
  );
449
563
  if (contentParts.length > 0) {
450
564
  const converted = contentParts.map((part) => {
451
565
  if (part.type === "image") {
452
- const url = part.source?.type === "base64" ? `data:${part.source.media_type};base64,${part.source.data}` : part.source?.url ?? "";
453
- 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;
454
576
  }
455
577
  return { type: "text", text: part.text };
456
- });
457
- if (converted.every((p) => p.type === "text")) {
578
+ }).filter(Boolean);
579
+ if (converted.length === 0) {
580
+ } else if (converted.every((p) => p.type === "text")) {
458
581
  messages.push({
459
582
  role: "user",
460
583
  content: converted.map((p) => p.text).join("")
@@ -495,6 +618,7 @@ function transformAnthropicToOpenAI(body) {
495
618
  if (body.max_tokens != null) result.max_tokens = body.max_tokens;
496
619
  if (body.temperature != null) result.temperature = body.temperature;
497
620
  if (body.tools?.length) {
621
+ debug(`transform: mapping ${body.tools.length} tool(s)`);
498
622
  result.tools = body.tools.map((t) => ({
499
623
  type: "function",
500
624
  function: {
@@ -518,6 +642,7 @@ function transformAnthropicToOpenAI(body) {
518
642
  return result;
519
643
  }
520
644
  function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
645
+ debug(`transform: openai -> anthropic model=${openaiResponse.model ?? originalModel} choices=${openaiResponse.choices?.length ?? 0}`);
521
646
  const choice = openaiResponse.choices?.[0];
522
647
  if (!choice) throw new Error("No choices in OpenAI response");
523
648
  const content = [];
@@ -622,6 +747,7 @@ import http from "http";
622
747
  async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
623
748
  const base = targetUrl.replace(/\/+$/, "");
624
749
  const server = http.createServer(async (req, res) => {
750
+ debug(`Proxy request: ${req.method} ${req.url}`);
625
751
  try {
626
752
  if (req.method === "GET" && req.url === "/v1/models") {
627
753
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -663,6 +789,7 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
663
789
  });
664
790
  if (!upstream2.ok) {
665
791
  const errText = await upstream2.text();
792
+ error(`Upstream streaming error: ${upstream2.status} ${errText}`);
666
793
  res.write(`event: error
667
794
  data: ${errText}
668
795
 
@@ -691,6 +818,7 @@ data: ${errText}
691
818
  });
692
819
  if (!upstream.ok) {
693
820
  const errText = await upstream.text();
821
+ error(`Upstream error: ${upstream.status} ${errText}`);
694
822
  res.writeHead(upstream.status, { "Content-Type": "application/json" });
695
823
  res.end(errText);
696
824
  return;
@@ -704,6 +832,7 @@ data: ${errText}
704
832
  res.writeHead(404, { "Content-Type": "application/json" });
705
833
  res.end(JSON.stringify({ error: { type: "not_found", message: "endpoint not found" } }));
706
834
  } catch (err) {
835
+ error("Proxy request handler error", err);
707
836
  if (!res.headersSent) {
708
837
  res.writeHead(500, { "Content-Type": "application/json" });
709
838
  res.end(JSON.stringify({ error: { type: "internal_error", message: String(err) } }));
@@ -714,9 +843,13 @@ data: ${errText}
714
843
  server.listen(0, "127.0.0.1", () => {
715
844
  const addr = server.address();
716
845
  const baseUrl = `http://127.0.0.1:${addr.port}`;
846
+ debug(`OpenAI proxy listening on ${baseUrl}`);
717
847
  resolve({
718
848
  baseUrl,
719
- stop: () => server.close()
849
+ stop: () => {
850
+ debug("OpenAI proxy stopped");
851
+ server.close();
852
+ }
720
853
  });
721
854
  });
722
855
  server.on("error", reject);
@@ -744,14 +877,14 @@ var PROVIDERS = [
744
877
  ];
745
878
  function providerCommand() {
746
879
  const cmd = new Command("provider").description("Manage provider types");
747
- cmd.command("list").description("List available provider types").action(() => {
880
+ cmd.command("list").description("List available provider types").action(safeAction(() => {
748
881
  const fmt = (name, desc) => `${name.padEnd(12)} ${desc}`;
749
882
  console.log(fmt("NAME", "DESCRIPTION"));
750
883
  console.log(fmt("----", "-----------"));
751
884
  for (const p of PROVIDERS) {
752
885
  console.log(fmt(p.name, p.description));
753
886
  }
754
- });
887
+ }));
755
888
  return cmd;
756
889
  }
757
890
 
@@ -760,11 +893,12 @@ function resolveClaudeBinary() {
760
893
  return createBinaryResolver().resolve();
761
894
  }
762
895
  function updateSettingsForProfile(p) {
896
+ debug(`updateSettingsForProfile: reading ${SETTINGS_FILE}`);
763
897
  ensureSettingsFile();
764
- const settings = readJson(SETTINGS_FILE);
898
+ const settings2 = readJson(SETTINGS_FILE);
765
899
  const models = p.models || (p.model ? [p.model] : []);
766
- delete settings.model;
767
- delete settings.availableModels;
900
+ delete settings2.model;
901
+ delete settings2.availableModels;
768
902
  const envVarsToClean = [
769
903
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
770
904
  "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
@@ -777,13 +911,14 @@ function updateSettingsForProfile(p) {
777
911
  "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
778
912
  "ANTHROPIC_CUSTOM_MODEL_OPTION"
779
913
  ];
780
- if (settings.env) {
781
- const env = settings.env;
914
+ if (settings2.env) {
915
+ const env = settings2.env;
782
916
  for (const key of envVarsToClean) {
783
917
  delete env[key];
784
918
  }
785
919
  }
786
- writeJson(SETTINGS_FILE, settings);
920
+ writeJson(SETTINGS_FILE, settings2);
921
+ debug(`updateSettingsForProfile: wrote ${SETTINGS_FILE}`);
787
922
  }
788
923
  function execClaude(profileName, p, extraArgs) {
789
924
  updateSettingsForProfile(p);
@@ -819,9 +954,10 @@ function execClaude(profileName, p, extraArgs) {
819
954
  env.ANTHROPIC_CUSTOM_MODEL_OPTION = models[0];
820
955
  }
821
956
  delete env.ANTHROPIC_API_KEY;
822
- console.error(`Using profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"}`);
957
+ info(`Launching Claude with profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"} binary=${binary}`);
823
958
  if (p.provider === "openai") {
824
959
  const allModels = p.models || (p.model ? [p.model] : []);
960
+ debug(`execClaude: starting OpenAI proxy for ${allModels.length} model(s)`);
825
961
  startOpenAIProxy(
826
962
  p.url || "https://api.openai.com",
827
963
  p.token || "",
@@ -829,12 +965,14 @@ function execClaude(profileName, p, extraArgs) {
829
965
  allModels
830
966
  ).then(({ baseUrl, stop }) => {
831
967
  env.ANTHROPIC_BASE_URL = baseUrl;
968
+ debug(`execClaude: proxy running at ${baseUrl}`);
832
969
  const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env, shell: process.platform === "win32" });
833
970
  child.on("close", (code) => {
834
971
  stop();
835
972
  process.exit(code ?? 1);
836
973
  });
837
974
  }).catch((err) => {
975
+ error("Failed to start OpenAI proxy", err);
838
976
  console.error("Failed to start OpenAI proxy:", err);
839
977
  process.exit(1);
840
978
  });
@@ -890,11 +1028,10 @@ function collect(value, previous) {
890
1028
  function profileCommand() {
891
1029
  const profile = new Command2("profile").description("Manage Claude CLI profiles");
892
1030
  const syncer = createProfileSyncer();
893
- 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((name, opts) => {
1031
+ 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) => {
894
1032
  const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
895
1033
  if (models && models.length > 3) {
896
- console.error("Error: A profile can have at most 3 models.");
897
- process.exit(1);
1034
+ throw new Error("Error: A profile can have at most 3 models.");
898
1035
  }
899
1036
  ensureProfilesFile();
900
1037
  const data = readJson(PROFILES_FILE);
@@ -907,16 +1044,17 @@ function profileCommand() {
907
1044
  if (opts.url) profile2.url = opts.url;
908
1045
  if (opts.provider) profile2.provider = opts.provider;
909
1046
  data.profiles[name] = profile2;
1047
+ debug(`profile add: syncing profile '${name}' to desktop`);
910
1048
  syncer.sync(name, profile2);
911
1049
  writeJson(PROFILES_FILE, data);
1050
+ debug(`profile add: wrote ${PROFILES_FILE}`);
912
1051
  console.log(`Profile '${name}' saved.`);
913
- });
914
- 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((name, opts) => {
1052
+ }));
1053
+ 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) => {
915
1054
  ensureProfilesFile();
916
1055
  const data = readJson(PROFILES_FILE);
917
1056
  if (!data.profiles[name]) {
918
- console.error(`Profile '${name}' not found. Use 'profile add' to create it.`);
919
- process.exit(1);
1057
+ throw new Error(`Profile '${name}' not found. Use 'profile add' to create it.`);
920
1058
  }
921
1059
  const p = data.profiles[name];
922
1060
  const providedModels = opts.model && opts.model.length > 0 ? opts.model : void 0;
@@ -962,17 +1100,18 @@ function profileCommand() {
962
1100
  }
963
1101
  const finalModels = p.models || (p.model ? [p.model] : []);
964
1102
  if (finalModels.length > 3) {
965
- console.error("Error: A profile can have at most 3 models.");
966
- process.exit(1);
1103
+ throw new Error("Error: A profile can have at most 3 models.");
967
1104
  }
968
1105
  if (opts.token) p.token = opts.token;
969
1106
  if (opts.url) p.url = opts.url;
970
1107
  if (opts.provider) p.provider = opts.provider;
1108
+ debug(`profile update: syncing profile '${name}' to desktop`);
971
1109
  syncer.sync(name, p);
972
1110
  writeJson(PROFILES_FILE, data);
1111
+ debug(`profile update: wrote ${PROFILES_FILE}`);
973
1112
  console.log(`Profile '${name}' updated.`);
974
- });
975
- profile.command("list").description("List all profiles").action(() => {
1113
+ }));
1114
+ profile.command("list").description("List all profiles").action(safeAction(() => {
976
1115
  ensureProfilesFile();
977
1116
  const data = readJson(PROFILES_FILE);
978
1117
  const profiles = data.profiles;
@@ -999,14 +1138,13 @@ function profileCommand() {
999
1138
  p.url || "(default)"
1000
1139
  ));
1001
1140
  }
1002
- });
1003
- profile.command("view").description("View full details of a profile (token unmasked)").argument("<name>", "Profile name").option("-j, --json", "Output as JSON").action((name, opts) => {
1141
+ }));
1142
+ 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) => {
1004
1143
  ensureProfilesFile();
1005
1144
  const data = readJson(PROFILES_FILE);
1006
1145
  const p = data.profiles[name];
1007
1146
  if (!p) {
1008
- console.error(`Profile '${name}' not found.`);
1009
- process.exit(1);
1147
+ throw new Error(`Profile '${name}' not found.`);
1010
1148
  }
1011
1149
  if (opts.json) {
1012
1150
  const { desktopId, ...rest } = p;
@@ -1034,29 +1172,28 @@ function profileCommand() {
1034
1172
  console.log(`URL: ${p.url || "(default)"}`);
1035
1173
  console.log(`Provider: ${p.provider || "anthropic"}`);
1036
1174
  }
1037
- });
1038
- profile.command("remove").description("Remove a profile").argument("<name>", "Profile name").action((name) => {
1175
+ }));
1176
+ profile.command("remove").description("Remove a profile").argument("<name>", "Profile name").action(safeAction((name) => {
1039
1177
  ensureProfilesFile();
1040
1178
  const data = readJson(PROFILES_FILE);
1041
1179
  if (!data.profiles[name]) {
1042
- console.error(`Profile '${name}' not found.`);
1043
- process.exit(1);
1180
+ throw new Error(`Profile '${name}' not found.`);
1044
1181
  }
1182
+ debug(`profile remove: removing profile '${name}' from desktop sync`);
1045
1183
  syncer.remove(name, data.profiles[name]);
1046
1184
  delete data.profiles[name];
1047
1185
  writeJson(PROFILES_FILE, data);
1186
+ debug(`profile remove: wrote ${PROFILES_FILE}`);
1048
1187
  console.log(`Profile '${name}' removed.`);
1049
- });
1050
- profile.command("rename").description("Rename a profile").argument("<oldName>", "Current profile name").argument("<newName>", "New profile name").action((oldName, newName) => {
1188
+ }));
1189
+ profile.command("rename").description("Rename a profile").argument("<oldName>", "Current profile name").argument("<newName>", "New profile name").action(safeAction((oldName, newName) => {
1051
1190
  ensureProfilesFile();
1052
1191
  const data = readJson(PROFILES_FILE);
1053
1192
  if (!data.profiles[oldName]) {
1054
- console.error(`Profile '${oldName}' not found.`);
1055
- process.exit(1);
1193
+ throw new Error(`Profile '${oldName}' not found.`);
1056
1194
  }
1057
1195
  if (data.profiles[newName]) {
1058
- console.error(`Profile '${newName}' already exists. Choose a different name.`);
1059
- process.exit(1);
1196
+ throw new Error(`Profile '${newName}' already exists. Choose a different name.`);
1060
1197
  }
1061
1198
  data.profiles[newName] = data.profiles[oldName];
1062
1199
  delete data.profiles[oldName];
@@ -1065,23 +1202,23 @@ function profileCommand() {
1065
1202
  }
1066
1203
  writeJson(PROFILES_FILE, data);
1067
1204
  console.log(`Profile '${oldName}' renamed to '${newName}'.`);
1068
- });
1069
- profile.command("default").description("Set the default profile").argument("<name>", "Profile name to set as default").action((name) => {
1205
+ }));
1206
+ profile.command("default").description("Set the default profile").argument("<name>", "Profile name to set as default").action(safeAction((name) => {
1070
1207
  ensureProfilesFile();
1071
1208
  const data = readJson(PROFILES_FILE);
1072
1209
  if (!data.profiles[name]) {
1073
- console.error(`Profile '${name}' not found.`);
1074
- process.exit(1);
1210
+ throw new Error(`Profile '${name}' not found.`);
1075
1211
  }
1076
1212
  data.default = name;
1213
+ debug(`profile default: setting active desktop profile to '${name}'`);
1077
1214
  syncer.setActive(data.profiles[name]);
1078
1215
  writeJson(PROFILES_FILE, data);
1216
+ debug(`profile default: wrote ${PROFILES_FILE}`);
1079
1217
  console.log(`Default profile set to '${name}'.`);
1080
- });
1081
- profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(() => {
1218
+ }));
1219
+ profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(safeAction(() => {
1082
1220
  if (!syncer.isSupported()) {
1083
- console.error("Claude desktop app is not installed.");
1084
- process.exit(1);
1221
+ throw new Error("Claude desktop app is not installed.");
1085
1222
  }
1086
1223
  ensureProfilesFile();
1087
1224
  const data = readJson(PROFILES_FILE);
@@ -1092,30 +1229,33 @@ function profileCommand() {
1092
1229
  }
1093
1230
  for (const name of names) {
1094
1231
  const p = data.profiles[name];
1232
+ debug(`profile sync: syncing '${name}' to desktop`);
1095
1233
  syncer.sync(name, p);
1096
1234
  }
1097
1235
  writeJson(PROFILES_FILE, data);
1236
+ debug(`profile sync: wrote ${PROFILES_FILE}`);
1098
1237
  console.log(`Synced ${names.length} profile(s) to the desktop app.`);
1099
- });
1238
+ }));
1100
1239
  return profile;
1101
1240
  }
1102
1241
  function useCommand() {
1103
1242
  const syncer = createProfileSyncer();
1104
- return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action((name) => {
1243
+ return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action(safeAction((name) => {
1105
1244
  ensureProfilesFile();
1106
1245
  const data = readJson(PROFILES_FILE);
1107
1246
  if (!data.profiles[name]) {
1108
- console.error(`Profile '${name}' not found.`);
1109
- process.exit(1);
1247
+ throw new Error(`Profile '${name}' not found.`);
1110
1248
  }
1111
1249
  data.default = name;
1250
+ debug(`use: setting active desktop profile to '${name}'`);
1112
1251
  syncer.setActive(data.profiles[name]);
1113
1252
  writeJson(PROFILES_FILE, data);
1253
+ debug(`use: wrote ${PROFILES_FILE}`);
1114
1254
  console.log(`Default profile set to '${name}'.`);
1115
- });
1255
+ }));
1116
1256
  }
1117
1257
  function runCommand() {
1118
- 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) => {
1258
+ 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) => {
1119
1259
  fixJsonFile(CLAUDE_JSON);
1120
1260
  ensureProfilesFile();
1121
1261
  const data = readJson(PROFILES_FILE);
@@ -1129,12 +1269,12 @@ function runCommand() {
1129
1269
  claudeArgs = args;
1130
1270
  }
1131
1271
  if (!profileName) {
1132
- console.error("No default profile set. Use 'cc-hub use <name>' first.");
1133
- process.exit(1);
1272
+ throw new Error("No default profile set. Use 'cc-hub use <name>' first.");
1134
1273
  }
1135
1274
  const p = data.profiles[profileName];
1275
+ debug(`run: launching claude with profile '${profileName}', args=[${claudeArgs.join(", ")}]`);
1136
1276
  execClaude(profileName, p, claudeArgs);
1137
- });
1277
+ }));
1138
1278
  }
1139
1279
 
1140
1280
  // src/hooks/commands.ts
@@ -1196,12 +1336,15 @@ function displayHookList(data) {
1196
1336
  }
1197
1337
  function hooksCommand() {
1198
1338
  const hooks = new Command3("hook").description("Manage Claude Code hooks in settings.json");
1199
- hooks.command("list").description("List all hooks").action(() => {
1339
+ hooks.command("list").description("List all hooks").action(safeAction(() => {
1340
+ debug("hook list: reading settings");
1200
1341
  ensureSettingsFile();
1201
1342
  const data = readJson(SETTINGS_FILE);
1343
+ debug(`hook list: found ${Object.keys(data.hooks || {}).length} event types`);
1202
1344
  displayHookList(data);
1203
- });
1204
- hooks.command("add").description("Add a hook to settings.json").requiredOption("-e, --event <event>", "Hook event (PreToolUse|PostToolUse|Notification|Stop|UserPromptSubmit|PermissionRequest)").option("-m, --matcher <matcher>", "Tool name matcher (omit for catch-all)").requiredOption("-c, --command <command>", "Shell command to run").option("-a, --async", "Run the hook asynchronously").action((opts) => {
1345
+ }));
1346
+ hooks.command("add").description("Add a hook to settings.json").requiredOption("-e, --event <event>", "Hook event (PreToolUse|PostToolUse|Notification|Stop|UserPromptSubmit|PermissionRequest)").option("-m, --matcher <matcher>", "Tool name matcher (omit for catch-all)").requiredOption("-c, --command <command>", "Shell command to run").option("-a, --async", "Run the hook asynchronously").action(safeAction((opts) => {
1347
+ debug(`hook add: event=${opts.event} matcher=${opts.matcher || "(any)"}`);
1205
1348
  ensureSettingsFile();
1206
1349
  const data = readJson(SETTINGS_FILE);
1207
1350
  const hooksRoot = data.hooks || (data.hooks = {});
@@ -1219,16 +1362,17 @@ function hooksCommand() {
1219
1362
  if (opts.async) newHook.async = true;
1220
1363
  targetGroup.hooks.push(newHook);
1221
1364
  writeJson(SETTINGS_FILE, data);
1365
+ debug(`hook add: wrote settings with new hook seq=${seq}`);
1222
1366
  console.log(`Hook added to event '${opts.event}'${matcher ? ` matcher='${matcher}'` : ""}.`);
1223
- });
1224
- hooks.command("remove").description("Remove a hook by its global index (see 'hooks list')").requiredOption("-i, --index <index>", "Global index from 'hooks list'", parseInt).action((opts) => {
1367
+ }));
1368
+ hooks.command("remove").description("Remove a hook by its global index (see 'hooks list')").requiredOption("-i, --index <index>", "Global index from 'hooks list'", parseInt).action(safeAction((opts) => {
1369
+ debug(`hook remove: index=${opts.index}`);
1225
1370
  ensureSettingsFile();
1226
1371
  const data = readJson(SETTINGS_FILE);
1227
1372
  const rows = buildFlat(data);
1228
1373
  const target = opts.index;
1229
1374
  if (target < 0 || target >= rows.length) {
1230
- console.error(`Index ${target} out of range (0-${rows.length - 1}).`);
1231
- process.exit(1);
1375
+ throw new Error(`Index ${target} out of range (0-${rows.length - 1}).`);
1232
1376
  }
1233
1377
  const r = rows[target];
1234
1378
  if (r.active) {
@@ -1242,13 +1386,15 @@ function hooksCommand() {
1242
1386
  if (pool.length === 0) delete data._cc_hub_disabled;
1243
1387
  }
1244
1388
  writeJson(SETTINGS_FILE, data);
1389
+ debug(`hook remove: wrote settings after removing hook ${target}`);
1245
1390
  console.log(`Hook ${target} removed.`);
1246
- });
1391
+ }));
1247
1392
  hooks.command("enable").description("Enable one or more disabled hooks").requiredOption("-i, --index <indexes...>", "Global index from 'hooks list' (repeatable)", (v, prev) => {
1248
1393
  prev = prev || [];
1249
1394
  prev.push(parseInt(v));
1250
1395
  return prev;
1251
- }).action((opts) => {
1396
+ }).action(safeAction((opts) => {
1397
+ debug(`hook enable: indexes=[${opts.index.join(", ")}]`);
1252
1398
  ensureSettingsFile();
1253
1399
  const data = readJson(SETTINGS_FILE);
1254
1400
  const rows = buildFlat(data);
@@ -1259,8 +1405,7 @@ function hooksCommand() {
1259
1405
  else if (rows[t].active) errors.push(`Index ${t} is already active.`);
1260
1406
  }
1261
1407
  if (errors.length > 0) {
1262
- for (const e of errors) console.error(e);
1263
- process.exit(1);
1408
+ throw new Error(errors.join("\n"));
1264
1409
  }
1265
1410
  const hooksRoot = data.hooks || (data.hooks = {});
1266
1411
  const pool = data._cc_hub_disabled;
@@ -1292,14 +1437,16 @@ function hooksCommand() {
1292
1437
  data._cc_hub_disabled = remaining;
1293
1438
  if (remaining.length === 0) delete data._cc_hub_disabled;
1294
1439
  writeJson(SETTINGS_FILE, data);
1440
+ debug(`hook enable: wrote settings after restoring ${toRestore.length} hook(s)`);
1295
1441
  console.log("");
1296
1442
  displayHookList(data);
1297
- });
1443
+ }));
1298
1444
  hooks.command("disable").description("Disable one or more hooks (removes from active)").requiredOption("-i, --index <indexes...>", "Global index from 'hooks list' (repeatable)", (v, prev) => {
1299
1445
  prev = prev || [];
1300
1446
  prev.push(parseInt(v));
1301
1447
  return prev;
1302
- }).action((opts) => {
1448
+ }).action(safeAction((opts) => {
1449
+ debug(`hook disable: indexes=[${opts.index.join(", ")}]`);
1303
1450
  ensureSettingsFile();
1304
1451
  const data = readJson(SETTINGS_FILE);
1305
1452
  const rows = buildFlat(data);
@@ -1310,8 +1457,7 @@ function hooksCommand() {
1310
1457
  else if (!rows[t].active) errors.push(`Index ${t} is already disabled.`);
1311
1458
  }
1312
1459
  if (errors.length > 0) {
1313
- for (const e of errors) console.error(e);
1314
- process.exit(1);
1460
+ throw new Error(errors.join("\n"));
1315
1461
  }
1316
1462
  const hooksRoot = data.hooks;
1317
1463
  const pool = data._cc_hub_disabled || (data._cc_hub_disabled = []);
@@ -1330,32 +1476,37 @@ function hooksCommand() {
1330
1476
  console.log(`Hook ${t} (${r.event}) disabled.`);
1331
1477
  }
1332
1478
  writeJson(SETTINGS_FILE, data);
1479
+ debug(`hook disable: wrote settings after disabling ${targets.length} hook(s)`);
1333
1480
  console.log("");
1334
1481
  displayHookList(data);
1335
- });
1482
+ }));
1336
1483
  return hooks;
1337
1484
  }
1338
1485
 
1339
1486
  // src/sessions/codec.ts
1340
1487
  function encodePath(p) {
1341
- return createPathCodec().encode(p);
1488
+ const encoded = createPathCodec().encode(p);
1489
+ debug(`codec: encode "${p}" -> "${encoded}"`);
1490
+ return encoded;
1342
1491
  }
1343
- function decodePath(encoded) {
1344
- return createPathCodec().decode(encoded);
1492
+ function decodePath2(encoded) {
1493
+ const decoded = createPathCodec().decode(encoded);
1494
+ debug(`codec: decode "${encoded}" -> "${decoded}"`);
1495
+ return decoded;
1345
1496
  }
1346
1497
 
1347
1498
  // src/sessions/stats.ts
1348
- import fs4 from "fs";
1349
- import path4 from "path";
1499
+ import fs5 from "fs";
1500
+ import path5 from "path";
1350
1501
  function getDirSize(dir) {
1351
1502
  let total = 0;
1352
1503
  try {
1353
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1354
- const fullPath = path4.join(dir, entry.name);
1504
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1505
+ const fullPath = path5.join(dir, entry.name);
1355
1506
  if (entry.isDirectory()) {
1356
1507
  total += getDirSize(fullPath);
1357
1508
  } else {
1358
- total += fs4.statSync(fullPath).size;
1509
+ total += fs5.statSync(fullPath).size;
1359
1510
  }
1360
1511
  }
1361
1512
  } catch {
@@ -1374,19 +1525,24 @@ function formatSize(bytes) {
1374
1525
  }
1375
1526
 
1376
1527
  // src/sessions/utils.ts
1377
- import fs5 from "fs";
1378
- import path5 from "path";
1528
+ import fs6 from "fs";
1529
+ import path6 from "path";
1379
1530
  function formatTimestamp(ms) {
1380
1531
  const d = new Date(ms);
1381
1532
  const pad = (n) => String(n).padStart(2, "0");
1382
1533
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1383
1534
  }
1384
1535
  function findProjectDir(query) {
1536
+ debug(`sessions: findProjectDir query="${query}"`);
1385
1537
  const encoded = encodePath(query);
1386
- if (fs5.existsSync(path5.join(PROJECTS_DIR, encoded))) return encoded;
1538
+ if (fs6.existsSync(path6.join(PROJECTS_DIR, encoded))) {
1539
+ debug(`sessions: findProjectDir exact match ${encoded}`);
1540
+ return encoded;
1541
+ }
1387
1542
  try {
1388
- const dirs = fs5.readdirSync(PROJECTS_DIR);
1543
+ const dirs = fs6.readdirSync(PROJECTS_DIR);
1389
1544
  const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
1545
+ if (match) debug(`sessions: findProjectDir partial match ${match}`);
1390
1546
  return match || null;
1391
1547
  } catch {
1392
1548
  return null;
@@ -1397,7 +1553,7 @@ function parseSessionMeta(filePath) {
1397
1553
  let slug = "";
1398
1554
  let customTitle = "";
1399
1555
  try {
1400
- const lines = fs5.readFileSync(filePath, "utf-8").split("\n");
1556
+ const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1401
1557
  for (const line of lines) {
1402
1558
  if (!line.trim()) continue;
1403
1559
  try {
@@ -1449,40 +1605,84 @@ function snippet(text, query, width = 150) {
1449
1605
  const suffix = end < text.length ? "..." : "";
1450
1606
  return prefix + text.slice(start, end) + suffix;
1451
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
+ }
1452
1650
 
1453
1651
  // src/sessions/commands.ts
1454
1652
  import { Command as Command4 } from "commander";
1455
- import fs6 from "fs";
1456
- import path6 from "path";
1653
+ import fs7 from "fs";
1654
+ import path7 from "path";
1655
+ import { spawnSync as spawnSync3 } from "child_process";
1457
1656
  function sessionCommand() {
1458
1657
  const session = new Command4("session").description("Manage Claude Code sessions");
1459
1658
  const desktopApp2 = createDesktopApp();
1460
1659
  const DESKTOP_SESSIONS_DIR2 = desktopApp2.getSessionsDir() || "";
1461
- 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) => {
1660
+ 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(safeAction((opts) => {
1661
+ debug(`session list: reading projects from ${PROJECTS_DIR}, limit=${opts.limit}`);
1462
1662
  const limit = parseInt(opts.limit, 10);
1463
1663
  let dirs;
1464
1664
  try {
1465
- dirs = fs6.readdirSync(PROJECTS_DIR);
1665
+ dirs = fs7.readdirSync(PROJECTS_DIR);
1466
1666
  } catch {
1467
1667
  console.log("No projects directory found.");
1468
1668
  return;
1469
1669
  }
1470
1670
  dirs.sort((a, b) => {
1471
- const statA = fs6.statSync(path6.join(PROJECTS_DIR, a));
1472
- const statB = fs6.statSync(path6.join(PROJECTS_DIR, b));
1671
+ const statA = fs7.statSync(path7.join(PROJECTS_DIR, a));
1672
+ const statB = fs7.statSync(path7.join(PROJECTS_DIR, b));
1473
1673
  return statB.mtimeMs - statA.mtimeMs;
1474
1674
  });
1475
1675
  let count = 0;
1476
1676
  for (const projDir of dirs) {
1477
1677
  if (count >= limit) break;
1478
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1678
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1479
1679
  let nSessions = 0;
1480
1680
  try {
1481
- nSessions = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1681
+ nSessions = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1482
1682
  } catch {
1483
1683
  }
1484
- const stat = fs6.statSync(fullPath);
1485
- const decoded = decodePath(projDir);
1684
+ const stat = fs7.statSync(fullPath);
1685
+ const decoded = decodePath2(projDir);
1486
1686
  if (opts.json) {
1487
1687
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
1488
1688
  } else if (opts.short) {
@@ -1492,15 +1692,15 @@ function sessionCommand() {
1492
1692
  }
1493
1693
  count++;
1494
1694
  }
1495
- });
1496
- session.command("show").description("Show session files for a project").argument("<project>", "Project path or encoded name (partial match ok)").option("-v, --verbose", "Show first user message of each session").action((project, opts) => {
1695
+ }));
1696
+ session.command("show").description("Show session files for a project").argument("<project>", "Project path or encoded name (partial match ok)").option("-v, --verbose", "Show first user message of each session").action(safeAction((project, opts) => {
1697
+ debug(`session show: project=${project} verbose=${!!opts.verbose}`);
1497
1698
  const projDir = findProjectDir(project);
1498
1699
  if (!projDir) {
1499
- console.error(`No project matched: ${project}`);
1500
- process.exit(1);
1700
+ throw new Error(`No project matched: ${project}`);
1501
1701
  }
1502
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1503
- console.log(`Project: ${decodePath(projDir)}`);
1702
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1703
+ console.log(`Project: ${decodePath2(projDir)}`);
1504
1704
  console.log(`Dir: ${fullPath}`);
1505
1705
  console.log("");
1506
1706
  const fmt = (sid, name, started, msgs) => `${sid.padEnd(36)} ${name.padEnd(30)} ${started.padEnd(17)} ${msgs}`;
@@ -1508,16 +1708,16 @@ function sessionCommand() {
1508
1708
  console.log(fmt("----------", "----", "-------", "--------"));
1509
1709
  let files;
1510
1710
  try {
1511
- files = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1711
+ files = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1512
1712
  } catch {
1513
1713
  return;
1514
1714
  }
1515
1715
  for (const file of files) {
1516
- const filePath = path6.join(fullPath, file);
1716
+ const filePath = path7.join(fullPath, file);
1517
1717
  const sessionId = file.replace(/\.jsonl$/, "");
1518
1718
  let msgCount = 0;
1519
1719
  try {
1520
- const content = fs6.readFileSync(filePath, "utf-8");
1720
+ const content = fs7.readFileSync(filePath, "utf-8");
1521
1721
  msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
1522
1722
  } catch {
1523
1723
  }
@@ -1525,7 +1725,7 @@ function sessionCommand() {
1525
1725
  console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
1526
1726
  if (opts.verbose) {
1527
1727
  try {
1528
- const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1728
+ const lines = fs7.readFileSync(filePath, "utf-8").split("\n");
1529
1729
  for (const line of lines) {
1530
1730
  if (!line.trim()) continue;
1531
1731
  try {
@@ -1551,8 +1751,9 @@ function sessionCommand() {
1551
1751
  }
1552
1752
  }
1553
1753
  }
1554
- });
1555
- session.command("search").description("Search conversation history across all projects").argument("<query>", "Text to search for").option("-p, --project <project>", "Filter to a specific project (partial match)").option("-n, --limit <n>", "Max number of matching files to show", "20").option("-i, --ignore-case", "Case-insensitive search").action((query, opts) => {
1754
+ }));
1755
+ session.command("search").description("Search conversation history across all projects").argument("<query>", "Text to search for").option("-p, --project <project>", "Filter to a specific project (partial match)").option("-n, --limit <n>", "Max number of matching files to show", "20").option("-i, --ignore-case", "Case-insensitive search").action(safeAction((query, opts) => {
1756
+ debug(`session search: query="${query}" project=${opts.project || "(all)"} limit=${opts.limit} ignoreCase=${!!opts.ignoreCase}`);
1556
1757
  let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
1557
1758
  if (isDesktopAppInstalled()) {
1558
1759
  searchRoots.push({ root: DESKTOP_SESSIONS_DIR2, label: "[desktop] " });
@@ -1560,10 +1761,9 @@ function sessionCommand() {
1560
1761
  if (opts.project) {
1561
1762
  const projDir = findProjectDir(opts.project);
1562
1763
  if (!projDir) {
1563
- console.error(`No project matched: ${opts.project}`);
1564
- process.exit(1);
1764
+ throw new Error(`No project matched: ${opts.project}`);
1565
1765
  }
1566
- searchRoots = [{ root: path6.join(PROJECTS_DIR, projDir), label: "" }];
1766
+ searchRoots = [{ root: path7.join(PROJECTS_DIR, projDir), label: "" }];
1567
1767
  }
1568
1768
  const limit = parseInt(opts.limit, 10);
1569
1769
  let count = 0;
@@ -1571,18 +1771,18 @@ function sessionCommand() {
1571
1771
  if (count >= limit) return;
1572
1772
  let entries;
1573
1773
  try {
1574
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1774
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1575
1775
  } catch {
1576
1776
  return;
1577
1777
  }
1578
1778
  for (const entry of entries) {
1579
1779
  if (count >= limit) break;
1580
- const fullPath = path6.join(dir, entry.name);
1780
+ const fullPath = path7.join(dir, entry.name);
1581
1781
  if (entry.isDirectory()) {
1582
1782
  searchDir(fullPath, label, baseDir);
1583
1783
  } else if (entry.name.endsWith(".jsonl")) {
1584
1784
  try {
1585
- const content = fs6.readFileSync(fullPath, "utf-8");
1785
+ const content = fs7.readFileSync(fullPath, "utf-8");
1586
1786
  const lines = content.split("\n");
1587
1787
  let found = false;
1588
1788
  for (let lineno = 0; lineno < lines.length; lineno++) {
@@ -1591,10 +1791,10 @@ function sessionCommand() {
1591
1791
  const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
1592
1792
  if (match) {
1593
1793
  if (!found) {
1594
- const relPath = path6.relative(baseDir, fullPath);
1595
- const projEnc = relPath.split(path6.sep)[0];
1596
- const sessionId = path6.basename(fullPath, ".jsonl");
1597
- const projName = label ? projEnc : decodePath(projEnc);
1794
+ const relPath = path7.relative(baseDir, fullPath);
1795
+ const projEnc = relPath.split(path7.sep)[0];
1796
+ const sessionId = path7.basename(fullPath, ".jsonl");
1797
+ const projName = label ? projEnc : decodePath2(projEnc);
1598
1798
  console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1599
1799
  found = true;
1600
1800
  count++;
@@ -1626,11 +1826,12 @@ function sessionCommand() {
1626
1826
  for (const { root, label } of searchRoots) {
1627
1827
  searchDir(root, label, root);
1628
1828
  }
1629
- });
1630
- session.command("ps").description("Show active Claude Code processes").action(() => {
1829
+ }));
1830
+ session.command("ps").description("Show active Claude Code processes").action(safeAction(() => {
1831
+ debug(`session ps: reading sessions from ${SESSIONS_DIR}`);
1631
1832
  let files;
1632
1833
  try {
1633
- files = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1834
+ files = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1634
1835
  } catch {
1635
1836
  console.log("(no session files found)");
1636
1837
  return;
@@ -1644,7 +1845,7 @@ function sessionCommand() {
1644
1845
  console.log(fmt("---", "----------", "-------", "---", ""));
1645
1846
  for (const file of files) {
1646
1847
  try {
1647
- const data = JSON.parse(fs6.readFileSync(path6.join(SESSIONS_DIR, file), "utf-8"));
1848
+ const data = JSON.parse(fs7.readFileSync(path7.join(SESSIONS_DIR, file), "utf-8"));
1648
1849
  const pid = String(data.pid || "?");
1649
1850
  const sessionId = data.sessionId || "?";
1650
1851
  const cwd = data.cwd || "?";
@@ -1659,8 +1860,9 @@ function sessionCommand() {
1659
1860
  } catch {
1660
1861
  }
1661
1862
  }
1662
- });
1663
- session.command("stats").description("Show summary statistics across all Claude Code sessions").action(() => {
1863
+ }));
1864
+ session.command("stats").description("Show summary statistics across all Claude Code sessions").action(safeAction(() => {
1865
+ debug(`session stats: scanning ${PROJECTS_DIR} and ${DESKTOP_SESSIONS_DIR2 || "(no desktop)"}`);
1664
1866
  let nProjects = 0;
1665
1867
  let nSessions = 0;
1666
1868
  let totalMsgs = 0;
@@ -1670,8 +1872,8 @@ function sessionCommand() {
1670
1872
  const walk = (dir) => {
1671
1873
  const results = [];
1672
1874
  try {
1673
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1674
- const fullPath = path6.join(dir, entry.name);
1875
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1876
+ const fullPath = path7.join(dir, entry.name);
1675
1877
  if (entry.isDirectory()) results.push(...walk(fullPath));
1676
1878
  else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
1677
1879
  }
@@ -1680,7 +1882,7 @@ function sessionCommand() {
1680
1882
  return results;
1681
1883
  };
1682
1884
  try {
1683
- nProjects = fs6.readdirSync(PROJECTS_DIR).length;
1885
+ nProjects = fs7.readdirSync(PROJECTS_DIR).length;
1684
1886
  } catch {
1685
1887
  }
1686
1888
  try {
@@ -1688,7 +1890,7 @@ function sessionCommand() {
1688
1890
  nSessions = sessionFiles.length;
1689
1891
  for (const f of sessionFiles) {
1690
1892
  try {
1691
- const content = fs6.readFileSync(f, "utf-8");
1893
+ const content = fs7.readFileSync(f, "utf-8");
1692
1894
  totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1693
1895
  } catch {
1694
1896
  }
@@ -1701,7 +1903,7 @@ function sessionCommand() {
1701
1903
  nDesktopSessions = desktopFiles.length;
1702
1904
  for (const f of desktopFiles) {
1703
1905
  try {
1704
- const content = fs6.readFileSync(f, "utf-8");
1906
+ const content = fs7.readFileSync(f, "utf-8");
1705
1907
  nDesktopMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1706
1908
  } catch {
1707
1909
  }
@@ -1710,7 +1912,7 @@ function sessionCommand() {
1710
1912
  }
1711
1913
  }
1712
1914
  try {
1713
- nActive = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1915
+ nActive = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1714
1916
  } catch {
1715
1917
  }
1716
1918
  console.log(`Projects: ${nProjects}`);
@@ -1733,8 +1935,9 @@ function sessionCommand() {
1733
1935
  if (desktopSize) {
1734
1936
  console.log(` Desktop: ${desktopSize}`);
1735
1937
  }
1736
- });
1737
- session.command("clean").description("Delete session JSONL files older than N days").option("-d, --days <n>", "Delete files older than this many days", "30").option("--dry-run", "Show what would be deleted without deleting").action((opts) => {
1938
+ }));
1939
+ session.command("clean").description("Delete session JSONL files older than N days").option("-d, --days <n>", "Delete files older than this many days", "30").option("--dry-run", "Show what would be deleted without deleting").action(safeAction((opts) => {
1940
+ debug(`session clean: days=${opts.days} dryRun=${!!opts.dryRun}`);
1738
1941
  const days = parseInt(opts.days, 10);
1739
1942
  const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1e3;
1740
1943
  let deleted = 0;
@@ -1742,23 +1945,23 @@ function sessionCommand() {
1742
1945
  const walk = (dir) => {
1743
1946
  let entries;
1744
1947
  try {
1745
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1948
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1746
1949
  } catch {
1747
1950
  return;
1748
1951
  }
1749
1952
  for (const entry of entries) {
1750
- const fullPath = path6.join(dir, entry.name);
1953
+ const fullPath = path7.join(dir, entry.name);
1751
1954
  if (entry.isDirectory()) {
1752
1955
  walk(fullPath);
1753
1956
  } else if (entry.name.endsWith(".jsonl")) {
1754
1957
  try {
1755
- const stat = fs6.statSync(fullPath);
1958
+ const stat = fs7.statSync(fullPath);
1756
1959
  if (stat.mtimeMs < cutoffMs) {
1757
1960
  const size = stat.size;
1758
1961
  if (opts.dryRun) {
1759
1962
  console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
1760
1963
  } else {
1761
- fs6.unlinkSync(fullPath);
1964
+ fs7.unlinkSync(fullPath);
1762
1965
  console.log(`Deleted: ${fullPath}`);
1763
1966
  }
1764
1967
  deleted++;
@@ -1776,7 +1979,39 @@ function sessionCommand() {
1776
1979
  console.log("");
1777
1980
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1778
1981
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
1779
- });
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
+ }));
1780
2015
  return session;
1781
2016
  }
1782
2017
 
@@ -1834,6 +2069,7 @@ _cc-hub() {
1834
2069
  'ps:Show active Claude Code processes'
1835
2070
  'stats:Show summary statistics'
1836
2071
  'clean:Delete session JSONL files older than N days'
2072
+ 'troubleshoot:Launch Claude Code to troubleshoot a session file'
1837
2073
  )
1838
2074
 
1839
2075
  _cc_hub_profiles() {
@@ -1911,6 +2147,8 @@ _cc-hub() {
1911
2147
  session)
1912
2148
  if (( CURRENT == 2 )); then
1913
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]'
1914
2152
  fi
1915
2153
  ;;
1916
2154
  esac
@@ -1970,7 +2208,7 @@ _cc-hub() {
1970
2208
  local provider_subcmds="list"
1971
2209
  local provider_types="anthropic openai"
1972
2210
  local hooks_subcmds="list add remove enable disable"
1973
- local session_subcmds="list show search ps stats clean"
2211
+ local session_subcmds="list show search ps stats clean troubleshoot"
1974
2212
 
1975
2213
  # Top-level command
1976
2214
  if [[ \${COMP_CWORD} -eq 1 ]]; then
@@ -2021,6 +2259,13 @@ _cc-hub() {
2021
2259
  session)
2022
2260
  if [[ \${COMP_CWORD} -eq 2 ]]; then
2023
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
2024
2269
  fi
2025
2270
  ;;
2026
2271
  esac
@@ -2048,7 +2293,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2048
2293
 
2049
2294
  $profileSubcmds = @('add', 'update', 'list', 'view', 'remove', 'rename', 'default', 'sync')
2050
2295
  $hookSubcmds = @('list', 'add', 'remove', 'enable', 'disable')
2051
- $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean')
2296
+ $sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean', 'troubleshoot')
2052
2297
  $providerSubcmds = @('list')
2053
2298
 
2054
2299
  $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
@@ -2090,7 +2335,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2090
2335
 
2091
2336
  // src/complete/index.ts
2092
2337
  function completionCommand() {
2093
- return new Command5("completion").description("Print shell completion script").addArgument(new Argument("<shell>", "Shell type: bash, zsh, or powershell").choices(["bash", "zsh", "powershell"])).action((shell) => {
2338
+ return new Command5("completion").description("Print shell completion script").addArgument(new Argument("<shell>", "Shell type: bash, zsh, or powershell").choices(["bash", "zsh", "powershell"])).action(safeAction((shell) => {
2094
2339
  switch (shell) {
2095
2340
  case "zsh":
2096
2341
  process.stdout.write(ZSH_COMPLETION);
@@ -2105,12 +2350,16 @@ function completionCommand() {
2105
2350
  console.error(`Unsupported shell: ${shell}. Use 'bash', 'zsh', or 'powershell'.`);
2106
2351
  process.exit(1);
2107
2352
  }
2108
- });
2353
+ }));
2109
2354
  }
2110
2355
 
2111
2356
  // src/index.ts
2112
2357
  var _require = createRequire(import.meta.url);
2113
2358
  var { version } = _require("../package.json");
2359
+ ensureSettingsFile();
2360
+ var settings = readJson(SETTINGS_FILE);
2361
+ setLogLevel(settings._cc_hub_logLevel || "INFO");
2362
+ installGlobalExceptionHandlers();
2114
2363
  var program = new Command6();
2115
2364
  program.name("cc-hub").description("Manage Claude CLI profiles, hooks, and sessions").version(version);
2116
2365
  program.addCommand(profileCommand());
@@ -2120,4 +2369,9 @@ program.addCommand(hooksCommand());
2120
2369
  program.addCommand(sessionCommand());
2121
2370
  program.addCommand(completionCommand());
2122
2371
  program.addCommand(providerCommand());
2123
- program.parse();
2372
+ try {
2373
+ program.parse();
2374
+ } catch (err) {
2375
+ console.error("Unexpected error:", err instanceof Error ? err.message : String(err));
2376
+ process.exit(1);
2377
+ }