cc-hub-cli 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +362 -202
  2. 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") {
@@ -495,6 +609,7 @@ function transformAnthropicToOpenAI(body) {
495
609
  if (body.max_tokens != null) result.max_tokens = body.max_tokens;
496
610
  if (body.temperature != null) result.temperature = body.temperature;
497
611
  if (body.tools?.length) {
612
+ debug(`transform: mapping ${body.tools.length} tool(s)`);
498
613
  result.tools = body.tools.map((t) => ({
499
614
  type: "function",
500
615
  function: {
@@ -518,6 +633,7 @@ function transformAnthropicToOpenAI(body) {
518
633
  return result;
519
634
  }
520
635
  function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
636
+ debug(`transform: openai -> anthropic model=${openaiResponse.model ?? originalModel} choices=${openaiResponse.choices?.length ?? 0}`);
521
637
  const choice = openaiResponse.choices?.[0];
522
638
  if (!choice) throw new Error("No choices in OpenAI response");
523
639
  const content = [];
@@ -622,6 +738,7 @@ import http from "http";
622
738
  async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
623
739
  const base = targetUrl.replace(/\/+$/, "");
624
740
  const server = http.createServer(async (req, res) => {
741
+ debug(`Proxy request: ${req.method} ${req.url}`);
625
742
  try {
626
743
  if (req.method === "GET" && req.url === "/v1/models") {
627
744
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -663,6 +780,7 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
663
780
  });
664
781
  if (!upstream2.ok) {
665
782
  const errText = await upstream2.text();
783
+ error(`Upstream streaming error: ${upstream2.status} ${errText}`);
666
784
  res.write(`event: error
667
785
  data: ${errText}
668
786
 
@@ -691,6 +809,7 @@ data: ${errText}
691
809
  });
692
810
  if (!upstream.ok) {
693
811
  const errText = await upstream.text();
812
+ error(`Upstream error: ${upstream.status} ${errText}`);
694
813
  res.writeHead(upstream.status, { "Content-Type": "application/json" });
695
814
  res.end(errText);
696
815
  return;
@@ -704,6 +823,7 @@ data: ${errText}
704
823
  res.writeHead(404, { "Content-Type": "application/json" });
705
824
  res.end(JSON.stringify({ error: { type: "not_found", message: "endpoint not found" } }));
706
825
  } catch (err) {
826
+ error("Proxy request handler error", err);
707
827
  if (!res.headersSent) {
708
828
  res.writeHead(500, { "Content-Type": "application/json" });
709
829
  res.end(JSON.stringify({ error: { type: "internal_error", message: String(err) } }));
@@ -714,9 +834,13 @@ data: ${errText}
714
834
  server.listen(0, "127.0.0.1", () => {
715
835
  const addr = server.address();
716
836
  const baseUrl = `http://127.0.0.1:${addr.port}`;
837
+ debug(`OpenAI proxy listening on ${baseUrl}`);
717
838
  resolve({
718
839
  baseUrl,
719
- stop: () => server.close()
840
+ stop: () => {
841
+ debug("OpenAI proxy stopped");
842
+ server.close();
843
+ }
720
844
  });
721
845
  });
722
846
  server.on("error", reject);
@@ -744,14 +868,14 @@ var PROVIDERS = [
744
868
  ];
745
869
  function providerCommand() {
746
870
  const cmd = new Command("provider").description("Manage provider types");
747
- cmd.command("list").description("List available provider types").action(() => {
871
+ cmd.command("list").description("List available provider types").action(safeAction(() => {
748
872
  const fmt = (name, desc) => `${name.padEnd(12)} ${desc}`;
749
873
  console.log(fmt("NAME", "DESCRIPTION"));
750
874
  console.log(fmt("----", "-----------"));
751
875
  for (const p of PROVIDERS) {
752
876
  console.log(fmt(p.name, p.description));
753
877
  }
754
- });
878
+ }));
755
879
  return cmd;
756
880
  }
757
881
 
@@ -760,11 +884,12 @@ function resolveClaudeBinary() {
760
884
  return createBinaryResolver().resolve();
761
885
  }
762
886
  function updateSettingsForProfile(p) {
887
+ debug(`updateSettingsForProfile: reading ${SETTINGS_FILE}`);
763
888
  ensureSettingsFile();
764
- const settings = readJson(SETTINGS_FILE);
889
+ const settings2 = readJson(SETTINGS_FILE);
765
890
  const models = p.models || (p.model ? [p.model] : []);
766
- delete settings.model;
767
- delete settings.availableModels;
891
+ delete settings2.model;
892
+ delete settings2.availableModels;
768
893
  const envVarsToClean = [
769
894
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
770
895
  "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
@@ -777,13 +902,14 @@ function updateSettingsForProfile(p) {
777
902
  "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
778
903
  "ANTHROPIC_CUSTOM_MODEL_OPTION"
779
904
  ];
780
- if (settings.env) {
781
- const env = settings.env;
905
+ if (settings2.env) {
906
+ const env = settings2.env;
782
907
  for (const key of envVarsToClean) {
783
908
  delete env[key];
784
909
  }
785
910
  }
786
- writeJson(SETTINGS_FILE, settings);
911
+ writeJson(SETTINGS_FILE, settings2);
912
+ debug(`updateSettingsForProfile: wrote ${SETTINGS_FILE}`);
787
913
  }
788
914
  function execClaude(profileName, p, extraArgs) {
789
915
  updateSettingsForProfile(p);
@@ -819,9 +945,10 @@ function execClaude(profileName, p, extraArgs) {
819
945
  env.ANTHROPIC_CUSTOM_MODEL_OPTION = models[0];
820
946
  }
821
947
  delete env.ANTHROPIC_API_KEY;
822
- console.error(`Using profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"}`);
948
+ info(`Launching Claude with profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"} binary=${binary}`);
823
949
  if (p.provider === "openai") {
824
950
  const allModels = p.models || (p.model ? [p.model] : []);
951
+ debug(`execClaude: starting OpenAI proxy for ${allModels.length} model(s)`);
825
952
  startOpenAIProxy(
826
953
  p.url || "https://api.openai.com",
827
954
  p.token || "",
@@ -829,12 +956,14 @@ function execClaude(profileName, p, extraArgs) {
829
956
  allModels
830
957
  ).then(({ baseUrl, stop }) => {
831
958
  env.ANTHROPIC_BASE_URL = baseUrl;
959
+ debug(`execClaude: proxy running at ${baseUrl}`);
832
960
  const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env, shell: process.platform === "win32" });
833
961
  child.on("close", (code) => {
834
962
  stop();
835
963
  process.exit(code ?? 1);
836
964
  });
837
965
  }).catch((err) => {
966
+ error("Failed to start OpenAI proxy", err);
838
967
  console.error("Failed to start OpenAI proxy:", err);
839
968
  process.exit(1);
840
969
  });
@@ -890,11 +1019,10 @@ function collect(value, previous) {
890
1019
  function profileCommand() {
891
1020
  const profile = new Command2("profile").description("Manage Claude CLI profiles");
892
1021
  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) => {
1022
+ profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID - can be used multiple times (max 3)", collect, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").option("-p, --provider <provider>", "Provider type: anthropic (default) or openai").action(safeAction((name, opts) => {
894
1023
  const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
895
1024
  if (models && models.length > 3) {
896
- console.error("Error: A profile can have at most 3 models.");
897
- process.exit(1);
1025
+ throw new Error("Error: A profile can have at most 3 models.");
898
1026
  }
899
1027
  ensureProfilesFile();
900
1028
  const data = readJson(PROFILES_FILE);
@@ -907,16 +1035,17 @@ function profileCommand() {
907
1035
  if (opts.url) profile2.url = opts.url;
908
1036
  if (opts.provider) profile2.provider = opts.provider;
909
1037
  data.profiles[name] = profile2;
1038
+ debug(`profile add: syncing profile '${name}' to desktop`);
910
1039
  syncer.sync(name, profile2);
911
1040
  writeJson(PROFILES_FILE, data);
1041
+ debug(`profile add: wrote ${PROFILES_FILE}`);
912
1042
  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) => {
1043
+ }));
1044
+ profile.command("update").description("Update fields of an existing profile").argument("<name>", "Profile name (must already exist)").option("-m, --model <model>", "Model ID - can be used multiple times", collect, []).option("-d, --delete-model <model>", "Remove model ID - can be used multiple times", collect, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL").option("-p, --provider <provider>", "Provider type").action(safeAction((name, opts) => {
915
1045
  ensureProfilesFile();
916
1046
  const data = readJson(PROFILES_FILE);
917
1047
  if (!data.profiles[name]) {
918
- console.error(`Profile '${name}' not found. Use 'profile add' to create it.`);
919
- process.exit(1);
1048
+ throw new Error(`Profile '${name}' not found. Use 'profile add' to create it.`);
920
1049
  }
921
1050
  const p = data.profiles[name];
922
1051
  const providedModels = opts.model && opts.model.length > 0 ? opts.model : void 0;
@@ -962,17 +1091,18 @@ function profileCommand() {
962
1091
  }
963
1092
  const finalModels = p.models || (p.model ? [p.model] : []);
964
1093
  if (finalModels.length > 3) {
965
- console.error("Error: A profile can have at most 3 models.");
966
- process.exit(1);
1094
+ throw new Error("Error: A profile can have at most 3 models.");
967
1095
  }
968
1096
  if (opts.token) p.token = opts.token;
969
1097
  if (opts.url) p.url = opts.url;
970
1098
  if (opts.provider) p.provider = opts.provider;
1099
+ debug(`profile update: syncing profile '${name}' to desktop`);
971
1100
  syncer.sync(name, p);
972
1101
  writeJson(PROFILES_FILE, data);
1102
+ debug(`profile update: wrote ${PROFILES_FILE}`);
973
1103
  console.log(`Profile '${name}' updated.`);
974
- });
975
- profile.command("list").description("List all profiles").action(() => {
1104
+ }));
1105
+ profile.command("list").description("List all profiles").action(safeAction(() => {
976
1106
  ensureProfilesFile();
977
1107
  const data = readJson(PROFILES_FILE);
978
1108
  const profiles = data.profiles;
@@ -999,14 +1129,13 @@ function profileCommand() {
999
1129
  p.url || "(default)"
1000
1130
  ));
1001
1131
  }
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) => {
1132
+ }));
1133
+ 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
1134
  ensureProfilesFile();
1005
1135
  const data = readJson(PROFILES_FILE);
1006
1136
  const p = data.profiles[name];
1007
1137
  if (!p) {
1008
- console.error(`Profile '${name}' not found.`);
1009
- process.exit(1);
1138
+ throw new Error(`Profile '${name}' not found.`);
1010
1139
  }
1011
1140
  if (opts.json) {
1012
1141
  const { desktopId, ...rest } = p;
@@ -1034,29 +1163,28 @@ function profileCommand() {
1034
1163
  console.log(`URL: ${p.url || "(default)"}`);
1035
1164
  console.log(`Provider: ${p.provider || "anthropic"}`);
1036
1165
  }
1037
- });
1038
- profile.command("remove").description("Remove a profile").argument("<name>", "Profile name").action((name) => {
1166
+ }));
1167
+ profile.command("remove").description("Remove a profile").argument("<name>", "Profile name").action(safeAction((name) => {
1039
1168
  ensureProfilesFile();
1040
1169
  const data = readJson(PROFILES_FILE);
1041
1170
  if (!data.profiles[name]) {
1042
- console.error(`Profile '${name}' not found.`);
1043
- process.exit(1);
1171
+ throw new Error(`Profile '${name}' not found.`);
1044
1172
  }
1173
+ debug(`profile remove: removing profile '${name}' from desktop sync`);
1045
1174
  syncer.remove(name, data.profiles[name]);
1046
1175
  delete data.profiles[name];
1047
1176
  writeJson(PROFILES_FILE, data);
1177
+ debug(`profile remove: wrote ${PROFILES_FILE}`);
1048
1178
  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) => {
1179
+ }));
1180
+ profile.command("rename").description("Rename a profile").argument("<oldName>", "Current profile name").argument("<newName>", "New profile name").action(safeAction((oldName, newName) => {
1051
1181
  ensureProfilesFile();
1052
1182
  const data = readJson(PROFILES_FILE);
1053
1183
  if (!data.profiles[oldName]) {
1054
- console.error(`Profile '${oldName}' not found.`);
1055
- process.exit(1);
1184
+ throw new Error(`Profile '${oldName}' not found.`);
1056
1185
  }
1057
1186
  if (data.profiles[newName]) {
1058
- console.error(`Profile '${newName}' already exists. Choose a different name.`);
1059
- process.exit(1);
1187
+ throw new Error(`Profile '${newName}' already exists. Choose a different name.`);
1060
1188
  }
1061
1189
  data.profiles[newName] = data.profiles[oldName];
1062
1190
  delete data.profiles[oldName];
@@ -1065,23 +1193,23 @@ function profileCommand() {
1065
1193
  }
1066
1194
  writeJson(PROFILES_FILE, data);
1067
1195
  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) => {
1196
+ }));
1197
+ profile.command("default").description("Set the default profile").argument("<name>", "Profile name to set as default").action(safeAction((name) => {
1070
1198
  ensureProfilesFile();
1071
1199
  const data = readJson(PROFILES_FILE);
1072
1200
  if (!data.profiles[name]) {
1073
- console.error(`Profile '${name}' not found.`);
1074
- process.exit(1);
1201
+ throw new Error(`Profile '${name}' not found.`);
1075
1202
  }
1076
1203
  data.default = name;
1204
+ debug(`profile default: setting active desktop profile to '${name}'`);
1077
1205
  syncer.setActive(data.profiles[name]);
1078
1206
  writeJson(PROFILES_FILE, data);
1207
+ debug(`profile default: wrote ${PROFILES_FILE}`);
1079
1208
  console.log(`Default profile set to '${name}'.`);
1080
- });
1081
- profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(() => {
1209
+ }));
1210
+ profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(safeAction(() => {
1082
1211
  if (!syncer.isSupported()) {
1083
- console.error("Claude desktop app is not installed.");
1084
- process.exit(1);
1212
+ throw new Error("Claude desktop app is not installed.");
1085
1213
  }
1086
1214
  ensureProfilesFile();
1087
1215
  const data = readJson(PROFILES_FILE);
@@ -1092,30 +1220,33 @@ function profileCommand() {
1092
1220
  }
1093
1221
  for (const name of names) {
1094
1222
  const p = data.profiles[name];
1223
+ debug(`profile sync: syncing '${name}' to desktop`);
1095
1224
  syncer.sync(name, p);
1096
1225
  }
1097
1226
  writeJson(PROFILES_FILE, data);
1227
+ debug(`profile sync: wrote ${PROFILES_FILE}`);
1098
1228
  console.log(`Synced ${names.length} profile(s) to the desktop app.`);
1099
- });
1229
+ }));
1100
1230
  return profile;
1101
1231
  }
1102
1232
  function useCommand() {
1103
1233
  const syncer = createProfileSyncer();
1104
- return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action((name) => {
1234
+ return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action(safeAction((name) => {
1105
1235
  ensureProfilesFile();
1106
1236
  const data = readJson(PROFILES_FILE);
1107
1237
  if (!data.profiles[name]) {
1108
- console.error(`Profile '${name}' not found.`);
1109
- process.exit(1);
1238
+ throw new Error(`Profile '${name}' not found.`);
1110
1239
  }
1111
1240
  data.default = name;
1241
+ debug(`use: setting active desktop profile to '${name}'`);
1112
1242
  syncer.setActive(data.profiles[name]);
1113
1243
  writeJson(PROFILES_FILE, data);
1244
+ debug(`use: wrote ${PROFILES_FILE}`);
1114
1245
  console.log(`Default profile set to '${name}'.`);
1115
- });
1246
+ }));
1116
1247
  }
1117
1248
  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) => {
1249
+ return new Command2("run").description("Launch Claude Code using the default or a specified profile").allowUnknownOption().argument("[args...]", "Optional profile name followed by extra arguments").action(safeAction((args) => {
1119
1250
  fixJsonFile(CLAUDE_JSON);
1120
1251
  ensureProfilesFile();
1121
1252
  const data = readJson(PROFILES_FILE);
@@ -1129,12 +1260,12 @@ function runCommand() {
1129
1260
  claudeArgs = args;
1130
1261
  }
1131
1262
  if (!profileName) {
1132
- console.error("No default profile set. Use 'cc-hub use <name>' first.");
1133
- process.exit(1);
1263
+ throw new Error("No default profile set. Use 'cc-hub use <name>' first.");
1134
1264
  }
1135
1265
  const p = data.profiles[profileName];
1266
+ debug(`run: launching claude with profile '${profileName}', args=[${claudeArgs.join(", ")}]`);
1136
1267
  execClaude(profileName, p, claudeArgs);
1137
- });
1268
+ }));
1138
1269
  }
1139
1270
 
1140
1271
  // src/hooks/commands.ts
@@ -1196,12 +1327,15 @@ function displayHookList(data) {
1196
1327
  }
1197
1328
  function hooksCommand() {
1198
1329
  const hooks = new Command3("hook").description("Manage Claude Code hooks in settings.json");
1199
- hooks.command("list").description("List all hooks").action(() => {
1330
+ hooks.command("list").description("List all hooks").action(safeAction(() => {
1331
+ debug("hook list: reading settings");
1200
1332
  ensureSettingsFile();
1201
1333
  const data = readJson(SETTINGS_FILE);
1334
+ debug(`hook list: found ${Object.keys(data.hooks || {}).length} event types`);
1202
1335
  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) => {
1336
+ }));
1337
+ 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) => {
1338
+ debug(`hook add: event=${opts.event} matcher=${opts.matcher || "(any)"}`);
1205
1339
  ensureSettingsFile();
1206
1340
  const data = readJson(SETTINGS_FILE);
1207
1341
  const hooksRoot = data.hooks || (data.hooks = {});
@@ -1219,16 +1353,17 @@ function hooksCommand() {
1219
1353
  if (opts.async) newHook.async = true;
1220
1354
  targetGroup.hooks.push(newHook);
1221
1355
  writeJson(SETTINGS_FILE, data);
1356
+ debug(`hook add: wrote settings with new hook seq=${seq}`);
1222
1357
  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) => {
1358
+ }));
1359
+ 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) => {
1360
+ debug(`hook remove: index=${opts.index}`);
1225
1361
  ensureSettingsFile();
1226
1362
  const data = readJson(SETTINGS_FILE);
1227
1363
  const rows = buildFlat(data);
1228
1364
  const target = opts.index;
1229
1365
  if (target < 0 || target >= rows.length) {
1230
- console.error(`Index ${target} out of range (0-${rows.length - 1}).`);
1231
- process.exit(1);
1366
+ throw new Error(`Index ${target} out of range (0-${rows.length - 1}).`);
1232
1367
  }
1233
1368
  const r = rows[target];
1234
1369
  if (r.active) {
@@ -1242,13 +1377,15 @@ function hooksCommand() {
1242
1377
  if (pool.length === 0) delete data._cc_hub_disabled;
1243
1378
  }
1244
1379
  writeJson(SETTINGS_FILE, data);
1380
+ debug(`hook remove: wrote settings after removing hook ${target}`);
1245
1381
  console.log(`Hook ${target} removed.`);
1246
- });
1382
+ }));
1247
1383
  hooks.command("enable").description("Enable one or more disabled hooks").requiredOption("-i, --index <indexes...>", "Global index from 'hooks list' (repeatable)", (v, prev) => {
1248
1384
  prev = prev || [];
1249
1385
  prev.push(parseInt(v));
1250
1386
  return prev;
1251
- }).action((opts) => {
1387
+ }).action(safeAction((opts) => {
1388
+ debug(`hook enable: indexes=[${opts.index.join(", ")}]`);
1252
1389
  ensureSettingsFile();
1253
1390
  const data = readJson(SETTINGS_FILE);
1254
1391
  const rows = buildFlat(data);
@@ -1259,8 +1396,7 @@ function hooksCommand() {
1259
1396
  else if (rows[t].active) errors.push(`Index ${t} is already active.`);
1260
1397
  }
1261
1398
  if (errors.length > 0) {
1262
- for (const e of errors) console.error(e);
1263
- process.exit(1);
1399
+ throw new Error(errors.join("\n"));
1264
1400
  }
1265
1401
  const hooksRoot = data.hooks || (data.hooks = {});
1266
1402
  const pool = data._cc_hub_disabled;
@@ -1292,14 +1428,16 @@ function hooksCommand() {
1292
1428
  data._cc_hub_disabled = remaining;
1293
1429
  if (remaining.length === 0) delete data._cc_hub_disabled;
1294
1430
  writeJson(SETTINGS_FILE, data);
1431
+ debug(`hook enable: wrote settings after restoring ${toRestore.length} hook(s)`);
1295
1432
  console.log("");
1296
1433
  displayHookList(data);
1297
- });
1434
+ }));
1298
1435
  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
1436
  prev = prev || [];
1300
1437
  prev.push(parseInt(v));
1301
1438
  return prev;
1302
- }).action((opts) => {
1439
+ }).action(safeAction((opts) => {
1440
+ debug(`hook disable: indexes=[${opts.index.join(", ")}]`);
1303
1441
  ensureSettingsFile();
1304
1442
  const data = readJson(SETTINGS_FILE);
1305
1443
  const rows = buildFlat(data);
@@ -1310,8 +1448,7 @@ function hooksCommand() {
1310
1448
  else if (!rows[t].active) errors.push(`Index ${t} is already disabled.`);
1311
1449
  }
1312
1450
  if (errors.length > 0) {
1313
- for (const e of errors) console.error(e);
1314
- process.exit(1);
1451
+ throw new Error(errors.join("\n"));
1315
1452
  }
1316
1453
  const hooksRoot = data.hooks;
1317
1454
  const pool = data._cc_hub_disabled || (data._cc_hub_disabled = []);
@@ -1330,32 +1467,37 @@ function hooksCommand() {
1330
1467
  console.log(`Hook ${t} (${r.event}) disabled.`);
1331
1468
  }
1332
1469
  writeJson(SETTINGS_FILE, data);
1470
+ debug(`hook disable: wrote settings after disabling ${targets.length} hook(s)`);
1333
1471
  console.log("");
1334
1472
  displayHookList(data);
1335
- });
1473
+ }));
1336
1474
  return hooks;
1337
1475
  }
1338
1476
 
1339
1477
  // src/sessions/codec.ts
1340
1478
  function encodePath(p) {
1341
- return createPathCodec().encode(p);
1479
+ const encoded = createPathCodec().encode(p);
1480
+ debug(`codec: encode "${p}" -> "${encoded}"`);
1481
+ return encoded;
1342
1482
  }
1343
1483
  function decodePath(encoded) {
1344
- return createPathCodec().decode(encoded);
1484
+ const decoded = createPathCodec().decode(encoded);
1485
+ debug(`codec: decode "${encoded}" -> "${decoded}"`);
1486
+ return decoded;
1345
1487
  }
1346
1488
 
1347
1489
  // src/sessions/stats.ts
1348
- import fs4 from "fs";
1349
- import path4 from "path";
1490
+ import fs5 from "fs";
1491
+ import path5 from "path";
1350
1492
  function getDirSize(dir) {
1351
1493
  let total = 0;
1352
1494
  try {
1353
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1354
- const fullPath = path4.join(dir, entry.name);
1495
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1496
+ const fullPath = path5.join(dir, entry.name);
1355
1497
  if (entry.isDirectory()) {
1356
1498
  total += getDirSize(fullPath);
1357
1499
  } else {
1358
- total += fs4.statSync(fullPath).size;
1500
+ total += fs5.statSync(fullPath).size;
1359
1501
  }
1360
1502
  }
1361
1503
  } catch {
@@ -1374,19 +1516,24 @@ function formatSize(bytes) {
1374
1516
  }
1375
1517
 
1376
1518
  // src/sessions/utils.ts
1377
- import fs5 from "fs";
1378
- import path5 from "path";
1519
+ import fs6 from "fs";
1520
+ import path6 from "path";
1379
1521
  function formatTimestamp(ms) {
1380
1522
  const d = new Date(ms);
1381
1523
  const pad = (n) => String(n).padStart(2, "0");
1382
1524
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1383
1525
  }
1384
1526
  function findProjectDir(query) {
1527
+ debug(`sessions: findProjectDir query="${query}"`);
1385
1528
  const encoded = encodePath(query);
1386
- if (fs5.existsSync(path5.join(PROJECTS_DIR, encoded))) return encoded;
1529
+ if (fs6.existsSync(path6.join(PROJECTS_DIR, encoded))) {
1530
+ debug(`sessions: findProjectDir exact match ${encoded}`);
1531
+ return encoded;
1532
+ }
1387
1533
  try {
1388
- const dirs = fs5.readdirSync(PROJECTS_DIR);
1534
+ const dirs = fs6.readdirSync(PROJECTS_DIR);
1389
1535
  const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
1536
+ if (match) debug(`sessions: findProjectDir partial match ${match}`);
1390
1537
  return match || null;
1391
1538
  } catch {
1392
1539
  return null;
@@ -1397,7 +1544,7 @@ function parseSessionMeta(filePath) {
1397
1544
  let slug = "";
1398
1545
  let customTitle = "";
1399
1546
  try {
1400
- const lines = fs5.readFileSync(filePath, "utf-8").split("\n");
1547
+ const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1401
1548
  for (const line of lines) {
1402
1549
  if (!line.trim()) continue;
1403
1550
  try {
@@ -1452,36 +1599,37 @@ function snippet(text, query, width = 150) {
1452
1599
 
1453
1600
  // src/sessions/commands.ts
1454
1601
  import { Command as Command4 } from "commander";
1455
- import fs6 from "fs";
1456
- import path6 from "path";
1602
+ import fs7 from "fs";
1603
+ import path7 from "path";
1457
1604
  function sessionCommand() {
1458
1605
  const session = new Command4("session").description("Manage Claude Code sessions");
1459
1606
  const desktopApp2 = createDesktopApp();
1460
1607
  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) => {
1608
+ 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) => {
1609
+ debug(`session list: reading projects from ${PROJECTS_DIR}, limit=${opts.limit}`);
1462
1610
  const limit = parseInt(opts.limit, 10);
1463
1611
  let dirs;
1464
1612
  try {
1465
- dirs = fs6.readdirSync(PROJECTS_DIR);
1613
+ dirs = fs7.readdirSync(PROJECTS_DIR);
1466
1614
  } catch {
1467
1615
  console.log("No projects directory found.");
1468
1616
  return;
1469
1617
  }
1470
1618
  dirs.sort((a, b) => {
1471
- const statA = fs6.statSync(path6.join(PROJECTS_DIR, a));
1472
- const statB = fs6.statSync(path6.join(PROJECTS_DIR, b));
1619
+ const statA = fs7.statSync(path7.join(PROJECTS_DIR, a));
1620
+ const statB = fs7.statSync(path7.join(PROJECTS_DIR, b));
1473
1621
  return statB.mtimeMs - statA.mtimeMs;
1474
1622
  });
1475
1623
  let count = 0;
1476
1624
  for (const projDir of dirs) {
1477
1625
  if (count >= limit) break;
1478
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1626
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1479
1627
  let nSessions = 0;
1480
1628
  try {
1481
- nSessions = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1629
+ nSessions = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1482
1630
  } catch {
1483
1631
  }
1484
- const stat = fs6.statSync(fullPath);
1632
+ const stat = fs7.statSync(fullPath);
1485
1633
  const decoded = decodePath(projDir);
1486
1634
  if (opts.json) {
1487
1635
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
@@ -1492,14 +1640,14 @@ function sessionCommand() {
1492
1640
  }
1493
1641
  count++;
1494
1642
  }
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) => {
1643
+ }));
1644
+ 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) => {
1645
+ debug(`session show: project=${project} verbose=${!!opts.verbose}`);
1497
1646
  const projDir = findProjectDir(project);
1498
1647
  if (!projDir) {
1499
- console.error(`No project matched: ${project}`);
1500
- process.exit(1);
1648
+ throw new Error(`No project matched: ${project}`);
1501
1649
  }
1502
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1650
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1503
1651
  console.log(`Project: ${decodePath(projDir)}`);
1504
1652
  console.log(`Dir: ${fullPath}`);
1505
1653
  console.log("");
@@ -1508,16 +1656,16 @@ function sessionCommand() {
1508
1656
  console.log(fmt("----------", "----", "-------", "--------"));
1509
1657
  let files;
1510
1658
  try {
1511
- files = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1659
+ files = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1512
1660
  } catch {
1513
1661
  return;
1514
1662
  }
1515
1663
  for (const file of files) {
1516
- const filePath = path6.join(fullPath, file);
1664
+ const filePath = path7.join(fullPath, file);
1517
1665
  const sessionId = file.replace(/\.jsonl$/, "");
1518
1666
  let msgCount = 0;
1519
1667
  try {
1520
- const content = fs6.readFileSync(filePath, "utf-8");
1668
+ const content = fs7.readFileSync(filePath, "utf-8");
1521
1669
  msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
1522
1670
  } catch {
1523
1671
  }
@@ -1525,7 +1673,7 @@ function sessionCommand() {
1525
1673
  console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
1526
1674
  if (opts.verbose) {
1527
1675
  try {
1528
- const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1676
+ const lines = fs7.readFileSync(filePath, "utf-8").split("\n");
1529
1677
  for (const line of lines) {
1530
1678
  if (!line.trim()) continue;
1531
1679
  try {
@@ -1551,8 +1699,9 @@ function sessionCommand() {
1551
1699
  }
1552
1700
  }
1553
1701
  }
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) => {
1702
+ }));
1703
+ 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) => {
1704
+ debug(`session search: query="${query}" project=${opts.project || "(all)"} limit=${opts.limit} ignoreCase=${!!opts.ignoreCase}`);
1556
1705
  let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
1557
1706
  if (isDesktopAppInstalled()) {
1558
1707
  searchRoots.push({ root: DESKTOP_SESSIONS_DIR2, label: "[desktop] " });
@@ -1560,10 +1709,9 @@ function sessionCommand() {
1560
1709
  if (opts.project) {
1561
1710
  const projDir = findProjectDir(opts.project);
1562
1711
  if (!projDir) {
1563
- console.error(`No project matched: ${opts.project}`);
1564
- process.exit(1);
1712
+ throw new Error(`No project matched: ${opts.project}`);
1565
1713
  }
1566
- searchRoots = [{ root: path6.join(PROJECTS_DIR, projDir), label: "" }];
1714
+ searchRoots = [{ root: path7.join(PROJECTS_DIR, projDir), label: "" }];
1567
1715
  }
1568
1716
  const limit = parseInt(opts.limit, 10);
1569
1717
  let count = 0;
@@ -1571,18 +1719,18 @@ function sessionCommand() {
1571
1719
  if (count >= limit) return;
1572
1720
  let entries;
1573
1721
  try {
1574
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1722
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1575
1723
  } catch {
1576
1724
  return;
1577
1725
  }
1578
1726
  for (const entry of entries) {
1579
1727
  if (count >= limit) break;
1580
- const fullPath = path6.join(dir, entry.name);
1728
+ const fullPath = path7.join(dir, entry.name);
1581
1729
  if (entry.isDirectory()) {
1582
1730
  searchDir(fullPath, label, baseDir);
1583
1731
  } else if (entry.name.endsWith(".jsonl")) {
1584
1732
  try {
1585
- const content = fs6.readFileSync(fullPath, "utf-8");
1733
+ const content = fs7.readFileSync(fullPath, "utf-8");
1586
1734
  const lines = content.split("\n");
1587
1735
  let found = false;
1588
1736
  for (let lineno = 0; lineno < lines.length; lineno++) {
@@ -1591,9 +1739,9 @@ function sessionCommand() {
1591
1739
  const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
1592
1740
  if (match) {
1593
1741
  if (!found) {
1594
- const relPath = path6.relative(baseDir, fullPath);
1595
- const projEnc = relPath.split(path6.sep)[0];
1596
- const sessionId = path6.basename(fullPath, ".jsonl");
1742
+ const relPath = path7.relative(baseDir, fullPath);
1743
+ const projEnc = relPath.split(path7.sep)[0];
1744
+ const sessionId = path7.basename(fullPath, ".jsonl");
1597
1745
  const projName = label ? projEnc : decodePath(projEnc);
1598
1746
  console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1599
1747
  found = true;
@@ -1626,11 +1774,12 @@ function sessionCommand() {
1626
1774
  for (const { root, label } of searchRoots) {
1627
1775
  searchDir(root, label, root);
1628
1776
  }
1629
- });
1630
- session.command("ps").description("Show active Claude Code processes").action(() => {
1777
+ }));
1778
+ session.command("ps").description("Show active Claude Code processes").action(safeAction(() => {
1779
+ debug(`session ps: reading sessions from ${SESSIONS_DIR}`);
1631
1780
  let files;
1632
1781
  try {
1633
- files = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1782
+ files = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1634
1783
  } catch {
1635
1784
  console.log("(no session files found)");
1636
1785
  return;
@@ -1644,7 +1793,7 @@ function sessionCommand() {
1644
1793
  console.log(fmt("---", "----------", "-------", "---", ""));
1645
1794
  for (const file of files) {
1646
1795
  try {
1647
- const data = JSON.parse(fs6.readFileSync(path6.join(SESSIONS_DIR, file), "utf-8"));
1796
+ const data = JSON.parse(fs7.readFileSync(path7.join(SESSIONS_DIR, file), "utf-8"));
1648
1797
  const pid = String(data.pid || "?");
1649
1798
  const sessionId = data.sessionId || "?";
1650
1799
  const cwd = data.cwd || "?";
@@ -1659,8 +1808,9 @@ function sessionCommand() {
1659
1808
  } catch {
1660
1809
  }
1661
1810
  }
1662
- });
1663
- session.command("stats").description("Show summary statistics across all Claude Code sessions").action(() => {
1811
+ }));
1812
+ session.command("stats").description("Show summary statistics across all Claude Code sessions").action(safeAction(() => {
1813
+ debug(`session stats: scanning ${PROJECTS_DIR} and ${DESKTOP_SESSIONS_DIR2 || "(no desktop)"}`);
1664
1814
  let nProjects = 0;
1665
1815
  let nSessions = 0;
1666
1816
  let totalMsgs = 0;
@@ -1670,8 +1820,8 @@ function sessionCommand() {
1670
1820
  const walk = (dir) => {
1671
1821
  const results = [];
1672
1822
  try {
1673
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1674
- const fullPath = path6.join(dir, entry.name);
1823
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1824
+ const fullPath = path7.join(dir, entry.name);
1675
1825
  if (entry.isDirectory()) results.push(...walk(fullPath));
1676
1826
  else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
1677
1827
  }
@@ -1680,7 +1830,7 @@ function sessionCommand() {
1680
1830
  return results;
1681
1831
  };
1682
1832
  try {
1683
- nProjects = fs6.readdirSync(PROJECTS_DIR).length;
1833
+ nProjects = fs7.readdirSync(PROJECTS_DIR).length;
1684
1834
  } catch {
1685
1835
  }
1686
1836
  try {
@@ -1688,7 +1838,7 @@ function sessionCommand() {
1688
1838
  nSessions = sessionFiles.length;
1689
1839
  for (const f of sessionFiles) {
1690
1840
  try {
1691
- const content = fs6.readFileSync(f, "utf-8");
1841
+ const content = fs7.readFileSync(f, "utf-8");
1692
1842
  totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1693
1843
  } catch {
1694
1844
  }
@@ -1701,7 +1851,7 @@ function sessionCommand() {
1701
1851
  nDesktopSessions = desktopFiles.length;
1702
1852
  for (const f of desktopFiles) {
1703
1853
  try {
1704
- const content = fs6.readFileSync(f, "utf-8");
1854
+ const content = fs7.readFileSync(f, "utf-8");
1705
1855
  nDesktopMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1706
1856
  } catch {
1707
1857
  }
@@ -1710,7 +1860,7 @@ function sessionCommand() {
1710
1860
  }
1711
1861
  }
1712
1862
  try {
1713
- nActive = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1863
+ nActive = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1714
1864
  } catch {
1715
1865
  }
1716
1866
  console.log(`Projects: ${nProjects}`);
@@ -1733,8 +1883,9 @@ function sessionCommand() {
1733
1883
  if (desktopSize) {
1734
1884
  console.log(` Desktop: ${desktopSize}`);
1735
1885
  }
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) => {
1886
+ }));
1887
+ 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) => {
1888
+ debug(`session clean: days=${opts.days} dryRun=${!!opts.dryRun}`);
1738
1889
  const days = parseInt(opts.days, 10);
1739
1890
  const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1e3;
1740
1891
  let deleted = 0;
@@ -1742,23 +1893,23 @@ function sessionCommand() {
1742
1893
  const walk = (dir) => {
1743
1894
  let entries;
1744
1895
  try {
1745
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1896
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1746
1897
  } catch {
1747
1898
  return;
1748
1899
  }
1749
1900
  for (const entry of entries) {
1750
- const fullPath = path6.join(dir, entry.name);
1901
+ const fullPath = path7.join(dir, entry.name);
1751
1902
  if (entry.isDirectory()) {
1752
1903
  walk(fullPath);
1753
1904
  } else if (entry.name.endsWith(".jsonl")) {
1754
1905
  try {
1755
- const stat = fs6.statSync(fullPath);
1906
+ const stat = fs7.statSync(fullPath);
1756
1907
  if (stat.mtimeMs < cutoffMs) {
1757
1908
  const size = stat.size;
1758
1909
  if (opts.dryRun) {
1759
1910
  console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
1760
1911
  } else {
1761
- fs6.unlinkSync(fullPath);
1912
+ fs7.unlinkSync(fullPath);
1762
1913
  console.log(`Deleted: ${fullPath}`);
1763
1914
  }
1764
1915
  deleted++;
@@ -1776,7 +1927,7 @@ function sessionCommand() {
1776
1927
  console.log("");
1777
1928
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1778
1929
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
1779
- });
1930
+ }));
1780
1931
  return session;
1781
1932
  }
1782
1933
 
@@ -2090,7 +2241,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2090
2241
 
2091
2242
  // src/complete/index.ts
2092
2243
  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) => {
2244
+ 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
2245
  switch (shell) {
2095
2246
  case "zsh":
2096
2247
  process.stdout.write(ZSH_COMPLETION);
@@ -2105,12 +2256,16 @@ function completionCommand() {
2105
2256
  console.error(`Unsupported shell: ${shell}. Use 'bash', 'zsh', or 'powershell'.`);
2106
2257
  process.exit(1);
2107
2258
  }
2108
- });
2259
+ }));
2109
2260
  }
2110
2261
 
2111
2262
  // src/index.ts
2112
2263
  var _require = createRequire(import.meta.url);
2113
2264
  var { version } = _require("../package.json");
2265
+ ensureSettingsFile();
2266
+ var settings = readJson(SETTINGS_FILE);
2267
+ setLogLevel(settings._cc_hub_logLevel || "INFO");
2268
+ installGlobalExceptionHandlers();
2114
2269
  var program = new Command6();
2115
2270
  program.name("cc-hub").description("Manage Claude CLI profiles, hooks, and sessions").version(version);
2116
2271
  program.addCommand(profileCommand());
@@ -2120,4 +2275,9 @@ program.addCommand(hooksCommand());
2120
2275
  program.addCommand(sessionCommand());
2121
2276
  program.addCommand(completionCommand());
2122
2277
  program.addCommand(providerCommand());
2123
- program.parse();
2278
+ try {
2279
+ program.parse();
2280
+ } catch (err) {
2281
+ console.error("Unexpected error:", err instanceof Error ? err.message : String(err));
2282
+ process.exit(1);
2283
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {