cc-hub-cli 1.1.3 → 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 +380 -196
  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,54 +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
- return this.isInstalled() ? path.join(this.supportDir, "configLibrary") : void 0;
138
+ if (!this.isInstalled()) return void 0;
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;
142
+ return configLib;
43
143
  }
44
144
  findBinary() {
45
- const claudeCodeDir = path.join(this.supportDir, "claude-code");
46
- 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;
47
148
  let versions;
48
149
  try {
49
- versions = fs.readdirSync(claudeCodeDir).filter(
50
- (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"))
51
152
  );
52
153
  } catch {
53
154
  return void 0;
54
155
  }
55
156
  if (versions.length === 0) return void 0;
56
157
  versions.sort(sortSemverDesc);
57
- 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;
58
161
  }
59
162
  };
60
163
  var WindowsDesktopApp = class {
61
164
  _buildCandidates() {
62
165
  const candidates = [
63
- path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude-3p"),
64
- path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude"),
65
- path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Claude-3p"),
66
- 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")
67
170
  ];
68
- const packagesDir = path.join(
69
- process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
171
+ const packagesDir = path2.join(
172
+ process.env.LOCALAPPDATA || path2.join(os2.homedir(), "AppData", "Local"),
70
173
  "Packages"
71
174
  );
72
- if (fs.existsSync(packagesDir)) {
175
+ if (fs2.existsSync(packagesDir)) {
73
176
  try {
74
- const entries = fs.readdirSync(packagesDir);
177
+ const entries = fs2.readdirSync(packagesDir);
75
178
  for (const entry of entries) {
76
179
  if (entry.startsWith("Claude_")) {
77
- candidates.push(path.join(packagesDir, entry, "LocalCache", "Roaming", "Claude-3p"));
78
- 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"));
79
182
  }
80
183
  }
81
184
  } catch {
@@ -85,7 +188,7 @@ var WindowsDesktopApp = class {
85
188
  }
86
189
  _findSupportDir() {
87
190
  for (const dir of this._buildCandidates()) {
88
- if (fs.existsSync(dir)) return dir;
191
+ if (fs2.existsSync(dir)) return dir;
89
192
  }
90
193
  return void 0;
91
194
  }
@@ -95,17 +198,37 @@ var WindowsDesktopApp = class {
95
198
  getSupportDir() {
96
199
  return this._findSupportDir();
97
200
  }
98
- getSessionsDir() {
201
+ getConfigLibrary() {
202
+ const candidates = this._buildCandidates();
203
+ for (const dir2 of candidates) {
204
+ const configLib = path2.join(dir2, "configLibrary");
205
+ if (fs2.existsSync(path2.join(configLib, "_meta.json"))) {
206
+ return configLib;
207
+ }
208
+ }
209
+ for (const dir2 of candidates) {
210
+ const configLib = path2.join(dir2, "configLibrary");
211
+ if (fs2.existsSync(configLib)) {
212
+ return configLib;
213
+ }
214
+ }
99
215
  const dir = this._findSupportDir();
100
- return dir ? path.join(dir, "local-agent-mode-sessions") : void 0;
216
+ return dir ? path2.join(dir, "configLibrary") : void 0;
101
217
  }
102
- getConfigLibrary() {
218
+ getSessionsDir() {
219
+ const candidates = this._buildCandidates();
220
+ for (const dir2 of candidates) {
221
+ const sessionsDir = path2.join(dir2, "local-agent-mode-sessions");
222
+ if (fs2.existsSync(sessionsDir)) {
223
+ return sessionsDir;
224
+ }
225
+ }
103
226
  const dir = this._findSupportDir();
104
- return dir ? path.join(dir, "configLibrary") : void 0;
227
+ return dir ? path2.join(dir, "local-agent-mode-sessions") : void 0;
105
228
  }
106
229
  findBinary() {
107
- const win32Binary = path.join(process.env.LOCALAPPDATA || "", "Programs", "Claude", "Claude.exe");
108
- 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;
109
232
  return void 0;
110
233
  }
111
234
  };
@@ -128,8 +251,8 @@ var NoOpDesktopApp = class {
128
251
  };
129
252
 
130
253
  // src/platform/profile-syncer.ts
131
- import fs2 from "fs";
132
- import path2 from "path";
254
+ import fs3 from "fs";
255
+ import path3 from "path";
133
256
  import { randomUUID } from "crypto";
134
257
  function toDesktopProfile(p) {
135
258
  const models = p.models || (p.model ? [p.model] : []);
@@ -158,9 +281,10 @@ var DesktopProfileSyncer = class {
158
281
  }
159
282
  sync(name, p) {
160
283
  const configLib = this.app.getConfigLibrary();
284
+ debug(`profile-syncer: sync '${name}' to ${configLib || "(none)"}`);
161
285
  if (!configLib) return;
162
- if (!fs2.existsSync(configLib)) {
163
- fs2.mkdirSync(configLib, { recursive: true });
286
+ if (!fs3.existsSync(configLib)) {
287
+ fs3.mkdirSync(configLib, { recursive: true });
164
288
  }
165
289
  const meta = this.readMeta();
166
290
  const entries = meta.entries || [];
@@ -183,9 +307,11 @@ var DesktopProfileSyncer = class {
183
307
  meta.entries = entries;
184
308
  this.writeMeta(meta);
185
309
  this.writeProfile(id, configLib, toDesktopProfile(p));
310
+ debug(`profile-syncer: synced '${name}' id=${id}`);
186
311
  }
187
312
  remove(name, p) {
188
313
  const configLib = this.app.getConfigLibrary();
314
+ debug(`profile-syncer: remove '${name}' id=${p.desktopId || "(none)"} from ${configLib || "(none)"}`);
189
315
  if (!configLib || !p.desktopId) return;
190
316
  const meta = this.readMeta();
191
317
  if (meta.entries) {
@@ -195,13 +321,14 @@ var DesktopProfileSyncer = class {
195
321
  delete meta.appliedId;
196
322
  }
197
323
  this.writeMeta(meta);
198
- const filePath = path2.join(configLib, `${p.desktopId}.json`);
199
- if (fs2.existsSync(filePath)) {
200
- fs2.unlinkSync(filePath);
324
+ const filePath = path3.join(configLib, `${p.desktopId}.json`);
325
+ if (fs3.existsSync(filePath)) {
326
+ fs3.unlinkSync(filePath);
201
327
  }
202
328
  }
203
329
  setActive(p) {
204
330
  const configLib = this.app.getConfigLibrary();
331
+ debug(`profile-syncer: setActive id=${p.desktopId || "(none)"} in ${configLib || "(none)"}`);
205
332
  if (!configLib || !p.desktopId) return;
206
333
  const meta = this.readMeta();
207
334
  meta.appliedId = p.desktopId;
@@ -214,11 +341,11 @@ var DesktopProfileSyncer = class {
214
341
  }
215
342
  metaFile() {
216
343
  const configLib = this.app.getConfigLibrary();
217
- return configLib ? path2.join(configLib, "_meta.json") : void 0;
344
+ return configLib ? path3.join(configLib, "_meta.json") : void 0;
218
345
  }
219
346
  readMeta() {
220
347
  const file = this.metaFile();
221
- if (!file || !fs2.existsSync(file)) return {};
348
+ if (!file || !fs3.existsSync(file)) return {};
222
349
  try {
223
350
  return readJson(file);
224
351
  } catch {
@@ -230,7 +357,7 @@ var DesktopProfileSyncer = class {
230
357
  if (file) writeJson(file, meta);
231
358
  }
232
359
  writeProfile(id, configLib, data) {
233
- writeJson(path2.join(configLib, `${id}.json`), data);
360
+ writeJson(path3.join(configLib, `${id}.json`), data);
234
361
  }
235
362
  };
236
363
 
@@ -242,21 +369,25 @@ var SystemBinaryResolver = class {
242
369
  }
243
370
  app;
244
371
  resolve() {
372
+ debug("binary-resolver: trying global 'claude' command");
245
373
  try {
246
374
  const result = spawnSync("claude", ["--version"], {
247
375
  shell: process.platform === "win32",
248
376
  encoding: "utf-8"
249
377
  });
250
378
  if (result.status === 0) {
379
+ debug("binary-resolver: found global 'claude'");
251
380
  return "claude";
252
381
  }
253
382
  } catch {
254
383
  }
384
+ debug("binary-resolver: trying desktop app binary");
255
385
  const desktopBinary = this.app.findBinary();
256
- if (desktopBinary) return desktopBinary;
257
- console.error("Error: Could not find Claude Code CLI.");
258
- console.error("Install it globally or install the Claude Code desktop app.");
259
- 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.");
260
391
  }
261
392
  };
262
393
 
@@ -300,30 +431,33 @@ function createPathCodec() {
300
431
  }
301
432
 
302
433
  // src/config.ts
303
- var CLAUDE_DIR = process.env.CLAUDE_DIR || path3.join(os2.homedir(), ".claude");
304
- var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path3.join(CLAUDE_DIR, "profiles.json");
305
- var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path3.join(CLAUDE_DIR, "settings.json");
306
- var CLAUDE_JSON = path3.join(os2.homedir(), ".claude.json");
307
- var PROJECTS_DIR = path3.join(CLAUDE_DIR, "projects");
308
- 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");
309
440
  var desktopApp = createDesktopApp();
310
441
  var DESKTOP_CONFIG_LIBRARY = desktopApp.getConfigLibrary() || "";
311
- 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") : "";
312
443
  var DESKTOP_SESSIONS_DIR = desktopApp.getSessionsDir() || "";
313
444
  function isDesktopAppInstalled() {
314
445
  return desktopApp.isInstalled();
315
446
  }
316
447
  function ensureFile(filePath, defaultContent) {
317
- if (!fs3.existsSync(filePath)) {
318
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
319
- 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");
320
452
  }
321
453
  }
322
454
  function readJson(filePath) {
323
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
455
+ debug(`readJson: ${filePath}`);
456
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
324
457
  }
325
458
  function writeJson(filePath, data) {
326
- 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");
327
461
  }
328
462
  function ensureProfilesFile() {
329
463
  ensureFile(PROFILES_FILE, '{"profiles":{}}\n');
@@ -332,12 +466,12 @@ function ensureSettingsFile() {
332
466
  ensureFile(SETTINGS_FILE, "{}\n");
333
467
  }
334
468
  function fixJsonFile(filePath, fallback = {}) {
335
- if (!fs3.existsSync(filePath)) return;
336
- const backupPath = path3.join(CLAUDE_DIR, path3.basename(filePath) + ".backup");
337
- 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");
338
472
  try {
339
473
  JSON.parse(raw);
340
- fs3.copyFileSync(filePath, backupPath);
474
+ fs4.copyFileSync(filePath, backupPath);
341
475
  return;
342
476
  } catch {
343
477
  }
@@ -362,15 +496,18 @@ function fixJsonFile(filePath, fallback = {}) {
362
496
  if (openCurly > 0) text += "}".repeat(openCurly);
363
497
  try {
364
498
  JSON.parse(text);
365
- fs3.writeFileSync(filePath, text + "\n", "utf-8");
366
- 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)}.`);
367
502
  } catch {
368
- if (fs3.existsSync(backupPath)) {
369
- fs3.copyFileSync(backupPath, filePath);
370
- 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.`);
371
507
  } else {
372
508
  writeJson(filePath, fallback);
373
- 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.`);
374
511
  }
375
512
  }
376
513
  }
@@ -390,6 +527,7 @@ function sanitizeToolId(id) {
390
527
  return sanitized;
391
528
  }
392
529
  function transformAnthropicToOpenAI(body) {
530
+ debug(`transform: anthropic -> openai model=${body.model} messages=${(body.messages ?? []).length}`);
393
531
  const messages = [];
394
532
  if (body.system) {
395
533
  if (typeof body.system === "string") {
@@ -471,6 +609,7 @@ function transformAnthropicToOpenAI(body) {
471
609
  if (body.max_tokens != null) result.max_tokens = body.max_tokens;
472
610
  if (body.temperature != null) result.temperature = body.temperature;
473
611
  if (body.tools?.length) {
612
+ debug(`transform: mapping ${body.tools.length} tool(s)`);
474
613
  result.tools = body.tools.map((t) => ({
475
614
  type: "function",
476
615
  function: {
@@ -494,6 +633,7 @@ function transformAnthropicToOpenAI(body) {
494
633
  return result;
495
634
  }
496
635
  function transformOpenAIResponseToAnthropic(openaiResponse, originalModel) {
636
+ debug(`transform: openai -> anthropic model=${openaiResponse.model ?? originalModel} choices=${openaiResponse.choices?.length ?? 0}`);
497
637
  const choice = openaiResponse.choices?.[0];
498
638
  if (!choice) throw new Error("No choices in OpenAI response");
499
639
  const content = [];
@@ -598,6 +738,7 @@ import http from "http";
598
738
  async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
599
739
  const base = targetUrl.replace(/\/+$/, "");
600
740
  const server = http.createServer(async (req, res) => {
741
+ debug(`Proxy request: ${req.method} ${req.url}`);
601
742
  try {
602
743
  if (req.method === "GET" && req.url === "/v1/models") {
603
744
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -639,6 +780,7 @@ async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
639
780
  });
640
781
  if (!upstream2.ok) {
641
782
  const errText = await upstream2.text();
783
+ error(`Upstream streaming error: ${upstream2.status} ${errText}`);
642
784
  res.write(`event: error
643
785
  data: ${errText}
644
786
 
@@ -667,6 +809,7 @@ data: ${errText}
667
809
  });
668
810
  if (!upstream.ok) {
669
811
  const errText = await upstream.text();
812
+ error(`Upstream error: ${upstream.status} ${errText}`);
670
813
  res.writeHead(upstream.status, { "Content-Type": "application/json" });
671
814
  res.end(errText);
672
815
  return;
@@ -680,6 +823,7 @@ data: ${errText}
680
823
  res.writeHead(404, { "Content-Type": "application/json" });
681
824
  res.end(JSON.stringify({ error: { type: "not_found", message: "endpoint not found" } }));
682
825
  } catch (err) {
826
+ error("Proxy request handler error", err);
683
827
  if (!res.headersSent) {
684
828
  res.writeHead(500, { "Content-Type": "application/json" });
685
829
  res.end(JSON.stringify({ error: { type: "internal_error", message: String(err) } }));
@@ -690,9 +834,13 @@ data: ${errText}
690
834
  server.listen(0, "127.0.0.1", () => {
691
835
  const addr = server.address();
692
836
  const baseUrl = `http://127.0.0.1:${addr.port}`;
837
+ debug(`OpenAI proxy listening on ${baseUrl}`);
693
838
  resolve({
694
839
  baseUrl,
695
- stop: () => server.close()
840
+ stop: () => {
841
+ debug("OpenAI proxy stopped");
842
+ server.close();
843
+ }
696
844
  });
697
845
  });
698
846
  server.on("error", reject);
@@ -720,14 +868,14 @@ var PROVIDERS = [
720
868
  ];
721
869
  function providerCommand() {
722
870
  const cmd = new Command("provider").description("Manage provider types");
723
- cmd.command("list").description("List available provider types").action(() => {
871
+ cmd.command("list").description("List available provider types").action(safeAction(() => {
724
872
  const fmt = (name, desc) => `${name.padEnd(12)} ${desc}`;
725
873
  console.log(fmt("NAME", "DESCRIPTION"));
726
874
  console.log(fmt("----", "-----------"));
727
875
  for (const p of PROVIDERS) {
728
876
  console.log(fmt(p.name, p.description));
729
877
  }
730
- });
878
+ }));
731
879
  return cmd;
732
880
  }
733
881
 
@@ -736,11 +884,12 @@ function resolveClaudeBinary() {
736
884
  return createBinaryResolver().resolve();
737
885
  }
738
886
  function updateSettingsForProfile(p) {
887
+ debug(`updateSettingsForProfile: reading ${SETTINGS_FILE}`);
739
888
  ensureSettingsFile();
740
- const settings = readJson(SETTINGS_FILE);
889
+ const settings2 = readJson(SETTINGS_FILE);
741
890
  const models = p.models || (p.model ? [p.model] : []);
742
- delete settings.model;
743
- delete settings.availableModels;
891
+ delete settings2.model;
892
+ delete settings2.availableModels;
744
893
  const envVarsToClean = [
745
894
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
746
895
  "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
@@ -753,13 +902,14 @@ function updateSettingsForProfile(p) {
753
902
  "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
754
903
  "ANTHROPIC_CUSTOM_MODEL_OPTION"
755
904
  ];
756
- if (settings.env) {
757
- const env = settings.env;
905
+ if (settings2.env) {
906
+ const env = settings2.env;
758
907
  for (const key of envVarsToClean) {
759
908
  delete env[key];
760
909
  }
761
910
  }
762
- writeJson(SETTINGS_FILE, settings);
911
+ writeJson(SETTINGS_FILE, settings2);
912
+ debug(`updateSettingsForProfile: wrote ${SETTINGS_FILE}`);
763
913
  }
764
914
  function execClaude(profileName, p, extraArgs) {
765
915
  updateSettingsForProfile(p);
@@ -795,9 +945,10 @@ function execClaude(profileName, p, extraArgs) {
795
945
  env.ANTHROPIC_CUSTOM_MODEL_OPTION = models[0];
796
946
  }
797
947
  delete env.ANTHROPIC_API_KEY;
798
- 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}`);
799
949
  if (p.provider === "openai") {
800
950
  const allModels = p.models || (p.model ? [p.model] : []);
951
+ debug(`execClaude: starting OpenAI proxy for ${allModels.length} model(s)`);
801
952
  startOpenAIProxy(
802
953
  p.url || "https://api.openai.com",
803
954
  p.token || "",
@@ -805,12 +956,14 @@ function execClaude(profileName, p, extraArgs) {
805
956
  allModels
806
957
  ).then(({ baseUrl, stop }) => {
807
958
  env.ANTHROPIC_BASE_URL = baseUrl;
959
+ debug(`execClaude: proxy running at ${baseUrl}`);
808
960
  const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env, shell: process.platform === "win32" });
809
961
  child.on("close", (code) => {
810
962
  stop();
811
963
  process.exit(code ?? 1);
812
964
  });
813
965
  }).catch((err) => {
966
+ error("Failed to start OpenAI proxy", err);
814
967
  console.error("Failed to start OpenAI proxy:", err);
815
968
  process.exit(1);
816
969
  });
@@ -866,11 +1019,10 @@ function collect(value, previous) {
866
1019
  function profileCommand() {
867
1020
  const profile = new Command2("profile").description("Manage Claude CLI profiles");
868
1021
  const syncer = createProfileSyncer();
869
- 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) => {
870
1023
  const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
871
1024
  if (models && models.length > 3) {
872
- console.error("Error: A profile can have at most 3 models.");
873
- process.exit(1);
1025
+ throw new Error("Error: A profile can have at most 3 models.");
874
1026
  }
875
1027
  ensureProfilesFile();
876
1028
  const data = readJson(PROFILES_FILE);
@@ -883,16 +1035,17 @@ function profileCommand() {
883
1035
  if (opts.url) profile2.url = opts.url;
884
1036
  if (opts.provider) profile2.provider = opts.provider;
885
1037
  data.profiles[name] = profile2;
1038
+ debug(`profile add: syncing profile '${name}' to desktop`);
886
1039
  syncer.sync(name, profile2);
887
1040
  writeJson(PROFILES_FILE, data);
1041
+ debug(`profile add: wrote ${PROFILES_FILE}`);
888
1042
  console.log(`Profile '${name}' saved.`);
889
- });
890
- 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) => {
891
1045
  ensureProfilesFile();
892
1046
  const data = readJson(PROFILES_FILE);
893
1047
  if (!data.profiles[name]) {
894
- console.error(`Profile '${name}' not found. Use 'profile add' to create it.`);
895
- process.exit(1);
1048
+ throw new Error(`Profile '${name}' not found. Use 'profile add' to create it.`);
896
1049
  }
897
1050
  const p = data.profiles[name];
898
1051
  const providedModels = opts.model && opts.model.length > 0 ? opts.model : void 0;
@@ -938,17 +1091,18 @@ function profileCommand() {
938
1091
  }
939
1092
  const finalModels = p.models || (p.model ? [p.model] : []);
940
1093
  if (finalModels.length > 3) {
941
- console.error("Error: A profile can have at most 3 models.");
942
- process.exit(1);
1094
+ throw new Error("Error: A profile can have at most 3 models.");
943
1095
  }
944
1096
  if (opts.token) p.token = opts.token;
945
1097
  if (opts.url) p.url = opts.url;
946
1098
  if (opts.provider) p.provider = opts.provider;
1099
+ debug(`profile update: syncing profile '${name}' to desktop`);
947
1100
  syncer.sync(name, p);
948
1101
  writeJson(PROFILES_FILE, data);
1102
+ debug(`profile update: wrote ${PROFILES_FILE}`);
949
1103
  console.log(`Profile '${name}' updated.`);
950
- });
951
- profile.command("list").description("List all profiles").action(() => {
1104
+ }));
1105
+ profile.command("list").description("List all profiles").action(safeAction(() => {
952
1106
  ensureProfilesFile();
953
1107
  const data = readJson(PROFILES_FILE);
954
1108
  const profiles = data.profiles;
@@ -975,14 +1129,13 @@ function profileCommand() {
975
1129
  p.url || "(default)"
976
1130
  ));
977
1131
  }
978
- });
979
- 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) => {
980
1134
  ensureProfilesFile();
981
1135
  const data = readJson(PROFILES_FILE);
982
1136
  const p = data.profiles[name];
983
1137
  if (!p) {
984
- console.error(`Profile '${name}' not found.`);
985
- process.exit(1);
1138
+ throw new Error(`Profile '${name}' not found.`);
986
1139
  }
987
1140
  if (opts.json) {
988
1141
  const { desktopId, ...rest } = p;
@@ -1010,29 +1163,28 @@ function profileCommand() {
1010
1163
  console.log(`URL: ${p.url || "(default)"}`);
1011
1164
  console.log(`Provider: ${p.provider || "anthropic"}`);
1012
1165
  }
1013
- });
1014
- 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) => {
1015
1168
  ensureProfilesFile();
1016
1169
  const data = readJson(PROFILES_FILE);
1017
1170
  if (!data.profiles[name]) {
1018
- console.error(`Profile '${name}' not found.`);
1019
- process.exit(1);
1171
+ throw new Error(`Profile '${name}' not found.`);
1020
1172
  }
1173
+ debug(`profile remove: removing profile '${name}' from desktop sync`);
1021
1174
  syncer.remove(name, data.profiles[name]);
1022
1175
  delete data.profiles[name];
1023
1176
  writeJson(PROFILES_FILE, data);
1177
+ debug(`profile remove: wrote ${PROFILES_FILE}`);
1024
1178
  console.log(`Profile '${name}' removed.`);
1025
- });
1026
- 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) => {
1027
1181
  ensureProfilesFile();
1028
1182
  const data = readJson(PROFILES_FILE);
1029
1183
  if (!data.profiles[oldName]) {
1030
- console.error(`Profile '${oldName}' not found.`);
1031
- process.exit(1);
1184
+ throw new Error(`Profile '${oldName}' not found.`);
1032
1185
  }
1033
1186
  if (data.profiles[newName]) {
1034
- console.error(`Profile '${newName}' already exists. Choose a different name.`);
1035
- process.exit(1);
1187
+ throw new Error(`Profile '${newName}' already exists. Choose a different name.`);
1036
1188
  }
1037
1189
  data.profiles[newName] = data.profiles[oldName];
1038
1190
  delete data.profiles[oldName];
@@ -1041,23 +1193,23 @@ function profileCommand() {
1041
1193
  }
1042
1194
  writeJson(PROFILES_FILE, data);
1043
1195
  console.log(`Profile '${oldName}' renamed to '${newName}'.`);
1044
- });
1045
- 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) => {
1046
1198
  ensureProfilesFile();
1047
1199
  const data = readJson(PROFILES_FILE);
1048
1200
  if (!data.profiles[name]) {
1049
- console.error(`Profile '${name}' not found.`);
1050
- process.exit(1);
1201
+ throw new Error(`Profile '${name}' not found.`);
1051
1202
  }
1052
1203
  data.default = name;
1204
+ debug(`profile default: setting active desktop profile to '${name}'`);
1053
1205
  syncer.setActive(data.profiles[name]);
1054
1206
  writeJson(PROFILES_FILE, data);
1207
+ debug(`profile default: wrote ${PROFILES_FILE}`);
1055
1208
  console.log(`Default profile set to '${name}'.`);
1056
- });
1057
- 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(() => {
1058
1211
  if (!syncer.isSupported()) {
1059
- console.error("Claude desktop app is not installed.");
1060
- process.exit(1);
1212
+ throw new Error("Claude desktop app is not installed.");
1061
1213
  }
1062
1214
  ensureProfilesFile();
1063
1215
  const data = readJson(PROFILES_FILE);
@@ -1068,30 +1220,33 @@ function profileCommand() {
1068
1220
  }
1069
1221
  for (const name of names) {
1070
1222
  const p = data.profiles[name];
1223
+ debug(`profile sync: syncing '${name}' to desktop`);
1071
1224
  syncer.sync(name, p);
1072
1225
  }
1073
1226
  writeJson(PROFILES_FILE, data);
1227
+ debug(`profile sync: wrote ${PROFILES_FILE}`);
1074
1228
  console.log(`Synced ${names.length} profile(s) to the desktop app.`);
1075
- });
1229
+ }));
1076
1230
  return profile;
1077
1231
  }
1078
1232
  function useCommand() {
1079
1233
  const syncer = createProfileSyncer();
1080
- 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) => {
1081
1235
  ensureProfilesFile();
1082
1236
  const data = readJson(PROFILES_FILE);
1083
1237
  if (!data.profiles[name]) {
1084
- console.error(`Profile '${name}' not found.`);
1085
- process.exit(1);
1238
+ throw new Error(`Profile '${name}' not found.`);
1086
1239
  }
1087
1240
  data.default = name;
1241
+ debug(`use: setting active desktop profile to '${name}'`);
1088
1242
  syncer.setActive(data.profiles[name]);
1089
1243
  writeJson(PROFILES_FILE, data);
1244
+ debug(`use: wrote ${PROFILES_FILE}`);
1090
1245
  console.log(`Default profile set to '${name}'.`);
1091
- });
1246
+ }));
1092
1247
  }
1093
1248
  function runCommand() {
1094
- 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) => {
1095
1250
  fixJsonFile(CLAUDE_JSON);
1096
1251
  ensureProfilesFile();
1097
1252
  const data = readJson(PROFILES_FILE);
@@ -1105,12 +1260,12 @@ function runCommand() {
1105
1260
  claudeArgs = args;
1106
1261
  }
1107
1262
  if (!profileName) {
1108
- console.error("No default profile set. Use 'cc-hub use <name>' first.");
1109
- process.exit(1);
1263
+ throw new Error("No default profile set. Use 'cc-hub use <name>' first.");
1110
1264
  }
1111
1265
  const p = data.profiles[profileName];
1266
+ debug(`run: launching claude with profile '${profileName}', args=[${claudeArgs.join(", ")}]`);
1112
1267
  execClaude(profileName, p, claudeArgs);
1113
- });
1268
+ }));
1114
1269
  }
1115
1270
 
1116
1271
  // src/hooks/commands.ts
@@ -1172,12 +1327,15 @@ function displayHookList(data) {
1172
1327
  }
1173
1328
  function hooksCommand() {
1174
1329
  const hooks = new Command3("hook").description("Manage Claude Code hooks in settings.json");
1175
- hooks.command("list").description("List all hooks").action(() => {
1330
+ hooks.command("list").description("List all hooks").action(safeAction(() => {
1331
+ debug("hook list: reading settings");
1176
1332
  ensureSettingsFile();
1177
1333
  const data = readJson(SETTINGS_FILE);
1334
+ debug(`hook list: found ${Object.keys(data.hooks || {}).length} event types`);
1178
1335
  displayHookList(data);
1179
- });
1180
- 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)"}`);
1181
1339
  ensureSettingsFile();
1182
1340
  const data = readJson(SETTINGS_FILE);
1183
1341
  const hooksRoot = data.hooks || (data.hooks = {});
@@ -1195,16 +1353,17 @@ function hooksCommand() {
1195
1353
  if (opts.async) newHook.async = true;
1196
1354
  targetGroup.hooks.push(newHook);
1197
1355
  writeJson(SETTINGS_FILE, data);
1356
+ debug(`hook add: wrote settings with new hook seq=${seq}`);
1198
1357
  console.log(`Hook added to event '${opts.event}'${matcher ? ` matcher='${matcher}'` : ""}.`);
1199
- });
1200
- 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}`);
1201
1361
  ensureSettingsFile();
1202
1362
  const data = readJson(SETTINGS_FILE);
1203
1363
  const rows = buildFlat(data);
1204
1364
  const target = opts.index;
1205
1365
  if (target < 0 || target >= rows.length) {
1206
- console.error(`Index ${target} out of range (0-${rows.length - 1}).`);
1207
- process.exit(1);
1366
+ throw new Error(`Index ${target} out of range (0-${rows.length - 1}).`);
1208
1367
  }
1209
1368
  const r = rows[target];
1210
1369
  if (r.active) {
@@ -1218,13 +1377,15 @@ function hooksCommand() {
1218
1377
  if (pool.length === 0) delete data._cc_hub_disabled;
1219
1378
  }
1220
1379
  writeJson(SETTINGS_FILE, data);
1380
+ debug(`hook remove: wrote settings after removing hook ${target}`);
1221
1381
  console.log(`Hook ${target} removed.`);
1222
- });
1382
+ }));
1223
1383
  hooks.command("enable").description("Enable one or more disabled hooks").requiredOption("-i, --index <indexes...>", "Global index from 'hooks list' (repeatable)", (v, prev) => {
1224
1384
  prev = prev || [];
1225
1385
  prev.push(parseInt(v));
1226
1386
  return prev;
1227
- }).action((opts) => {
1387
+ }).action(safeAction((opts) => {
1388
+ debug(`hook enable: indexes=[${opts.index.join(", ")}]`);
1228
1389
  ensureSettingsFile();
1229
1390
  const data = readJson(SETTINGS_FILE);
1230
1391
  const rows = buildFlat(data);
@@ -1235,8 +1396,7 @@ function hooksCommand() {
1235
1396
  else if (rows[t].active) errors.push(`Index ${t} is already active.`);
1236
1397
  }
1237
1398
  if (errors.length > 0) {
1238
- for (const e of errors) console.error(e);
1239
- process.exit(1);
1399
+ throw new Error(errors.join("\n"));
1240
1400
  }
1241
1401
  const hooksRoot = data.hooks || (data.hooks = {});
1242
1402
  const pool = data._cc_hub_disabled;
@@ -1268,14 +1428,16 @@ function hooksCommand() {
1268
1428
  data._cc_hub_disabled = remaining;
1269
1429
  if (remaining.length === 0) delete data._cc_hub_disabled;
1270
1430
  writeJson(SETTINGS_FILE, data);
1431
+ debug(`hook enable: wrote settings after restoring ${toRestore.length} hook(s)`);
1271
1432
  console.log("");
1272
1433
  displayHookList(data);
1273
- });
1434
+ }));
1274
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) => {
1275
1436
  prev = prev || [];
1276
1437
  prev.push(parseInt(v));
1277
1438
  return prev;
1278
- }).action((opts) => {
1439
+ }).action(safeAction((opts) => {
1440
+ debug(`hook disable: indexes=[${opts.index.join(", ")}]`);
1279
1441
  ensureSettingsFile();
1280
1442
  const data = readJson(SETTINGS_FILE);
1281
1443
  const rows = buildFlat(data);
@@ -1286,8 +1448,7 @@ function hooksCommand() {
1286
1448
  else if (!rows[t].active) errors.push(`Index ${t} is already disabled.`);
1287
1449
  }
1288
1450
  if (errors.length > 0) {
1289
- for (const e of errors) console.error(e);
1290
- process.exit(1);
1451
+ throw new Error(errors.join("\n"));
1291
1452
  }
1292
1453
  const hooksRoot = data.hooks;
1293
1454
  const pool = data._cc_hub_disabled || (data._cc_hub_disabled = []);
@@ -1306,32 +1467,37 @@ function hooksCommand() {
1306
1467
  console.log(`Hook ${t} (${r.event}) disabled.`);
1307
1468
  }
1308
1469
  writeJson(SETTINGS_FILE, data);
1470
+ debug(`hook disable: wrote settings after disabling ${targets.length} hook(s)`);
1309
1471
  console.log("");
1310
1472
  displayHookList(data);
1311
- });
1473
+ }));
1312
1474
  return hooks;
1313
1475
  }
1314
1476
 
1315
1477
  // src/sessions/codec.ts
1316
1478
  function encodePath(p) {
1317
- return createPathCodec().encode(p);
1479
+ const encoded = createPathCodec().encode(p);
1480
+ debug(`codec: encode "${p}" -> "${encoded}"`);
1481
+ return encoded;
1318
1482
  }
1319
1483
  function decodePath(encoded) {
1320
- return createPathCodec().decode(encoded);
1484
+ const decoded = createPathCodec().decode(encoded);
1485
+ debug(`codec: decode "${encoded}" -> "${decoded}"`);
1486
+ return decoded;
1321
1487
  }
1322
1488
 
1323
1489
  // src/sessions/stats.ts
1324
- import fs4 from "fs";
1325
- import path4 from "path";
1490
+ import fs5 from "fs";
1491
+ import path5 from "path";
1326
1492
  function getDirSize(dir) {
1327
1493
  let total = 0;
1328
1494
  try {
1329
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1330
- 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);
1331
1497
  if (entry.isDirectory()) {
1332
1498
  total += getDirSize(fullPath);
1333
1499
  } else {
1334
- total += fs4.statSync(fullPath).size;
1500
+ total += fs5.statSync(fullPath).size;
1335
1501
  }
1336
1502
  }
1337
1503
  } catch {
@@ -1350,19 +1516,24 @@ function formatSize(bytes) {
1350
1516
  }
1351
1517
 
1352
1518
  // src/sessions/utils.ts
1353
- import fs5 from "fs";
1354
- import path5 from "path";
1519
+ import fs6 from "fs";
1520
+ import path6 from "path";
1355
1521
  function formatTimestamp(ms) {
1356
1522
  const d = new Date(ms);
1357
1523
  const pad = (n) => String(n).padStart(2, "0");
1358
1524
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1359
1525
  }
1360
1526
  function findProjectDir(query) {
1527
+ debug(`sessions: findProjectDir query="${query}"`);
1361
1528
  const encoded = encodePath(query);
1362
- 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
+ }
1363
1533
  try {
1364
- const dirs = fs5.readdirSync(PROJECTS_DIR);
1534
+ const dirs = fs6.readdirSync(PROJECTS_DIR);
1365
1535
  const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
1536
+ if (match) debug(`sessions: findProjectDir partial match ${match}`);
1366
1537
  return match || null;
1367
1538
  } catch {
1368
1539
  return null;
@@ -1373,7 +1544,7 @@ function parseSessionMeta(filePath) {
1373
1544
  let slug = "";
1374
1545
  let customTitle = "";
1375
1546
  try {
1376
- const lines = fs5.readFileSync(filePath, "utf-8").split("\n");
1547
+ const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1377
1548
  for (const line of lines) {
1378
1549
  if (!line.trim()) continue;
1379
1550
  try {
@@ -1428,36 +1599,37 @@ function snippet(text, query, width = 150) {
1428
1599
 
1429
1600
  // src/sessions/commands.ts
1430
1601
  import { Command as Command4 } from "commander";
1431
- import fs6 from "fs";
1432
- import path6 from "path";
1602
+ import fs7 from "fs";
1603
+ import path7 from "path";
1433
1604
  function sessionCommand() {
1434
1605
  const session = new Command4("session").description("Manage Claude Code sessions");
1435
1606
  const desktopApp2 = createDesktopApp();
1436
1607
  const DESKTOP_SESSIONS_DIR2 = desktopApp2.getSessionsDir() || "";
1437
- 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}`);
1438
1610
  const limit = parseInt(opts.limit, 10);
1439
1611
  let dirs;
1440
1612
  try {
1441
- dirs = fs6.readdirSync(PROJECTS_DIR);
1613
+ dirs = fs7.readdirSync(PROJECTS_DIR);
1442
1614
  } catch {
1443
1615
  console.log("No projects directory found.");
1444
1616
  return;
1445
1617
  }
1446
1618
  dirs.sort((a, b) => {
1447
- const statA = fs6.statSync(path6.join(PROJECTS_DIR, a));
1448
- 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));
1449
1621
  return statB.mtimeMs - statA.mtimeMs;
1450
1622
  });
1451
1623
  let count = 0;
1452
1624
  for (const projDir of dirs) {
1453
1625
  if (count >= limit) break;
1454
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1626
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1455
1627
  let nSessions = 0;
1456
1628
  try {
1457
- nSessions = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1629
+ nSessions = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1458
1630
  } catch {
1459
1631
  }
1460
- const stat = fs6.statSync(fullPath);
1632
+ const stat = fs7.statSync(fullPath);
1461
1633
  const decoded = decodePath(projDir);
1462
1634
  if (opts.json) {
1463
1635
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
@@ -1468,14 +1640,14 @@ function sessionCommand() {
1468
1640
  }
1469
1641
  count++;
1470
1642
  }
1471
- });
1472
- 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}`);
1473
1646
  const projDir = findProjectDir(project);
1474
1647
  if (!projDir) {
1475
- console.error(`No project matched: ${project}`);
1476
- process.exit(1);
1648
+ throw new Error(`No project matched: ${project}`);
1477
1649
  }
1478
- const fullPath = path6.join(PROJECTS_DIR, projDir);
1650
+ const fullPath = path7.join(PROJECTS_DIR, projDir);
1479
1651
  console.log(`Project: ${decodePath(projDir)}`);
1480
1652
  console.log(`Dir: ${fullPath}`);
1481
1653
  console.log("");
@@ -1484,16 +1656,16 @@ function sessionCommand() {
1484
1656
  console.log(fmt("----------", "----", "-------", "--------"));
1485
1657
  let files;
1486
1658
  try {
1487
- files = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1659
+ files = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1488
1660
  } catch {
1489
1661
  return;
1490
1662
  }
1491
1663
  for (const file of files) {
1492
- const filePath = path6.join(fullPath, file);
1664
+ const filePath = path7.join(fullPath, file);
1493
1665
  const sessionId = file.replace(/\.jsonl$/, "");
1494
1666
  let msgCount = 0;
1495
1667
  try {
1496
- const content = fs6.readFileSync(filePath, "utf-8");
1668
+ const content = fs7.readFileSync(filePath, "utf-8");
1497
1669
  msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
1498
1670
  } catch {
1499
1671
  }
@@ -1501,7 +1673,7 @@ function sessionCommand() {
1501
1673
  console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
1502
1674
  if (opts.verbose) {
1503
1675
  try {
1504
- const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
1676
+ const lines = fs7.readFileSync(filePath, "utf-8").split("\n");
1505
1677
  for (const line of lines) {
1506
1678
  if (!line.trim()) continue;
1507
1679
  try {
@@ -1527,8 +1699,9 @@ function sessionCommand() {
1527
1699
  }
1528
1700
  }
1529
1701
  }
1530
- });
1531
- 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}`);
1532
1705
  let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
1533
1706
  if (isDesktopAppInstalled()) {
1534
1707
  searchRoots.push({ root: DESKTOP_SESSIONS_DIR2, label: "[desktop] " });
@@ -1536,10 +1709,9 @@ function sessionCommand() {
1536
1709
  if (opts.project) {
1537
1710
  const projDir = findProjectDir(opts.project);
1538
1711
  if (!projDir) {
1539
- console.error(`No project matched: ${opts.project}`);
1540
- process.exit(1);
1712
+ throw new Error(`No project matched: ${opts.project}`);
1541
1713
  }
1542
- searchRoots = [{ root: path6.join(PROJECTS_DIR, projDir), label: "" }];
1714
+ searchRoots = [{ root: path7.join(PROJECTS_DIR, projDir), label: "" }];
1543
1715
  }
1544
1716
  const limit = parseInt(opts.limit, 10);
1545
1717
  let count = 0;
@@ -1547,18 +1719,18 @@ function sessionCommand() {
1547
1719
  if (count >= limit) return;
1548
1720
  let entries;
1549
1721
  try {
1550
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1722
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1551
1723
  } catch {
1552
1724
  return;
1553
1725
  }
1554
1726
  for (const entry of entries) {
1555
1727
  if (count >= limit) break;
1556
- const fullPath = path6.join(dir, entry.name);
1728
+ const fullPath = path7.join(dir, entry.name);
1557
1729
  if (entry.isDirectory()) {
1558
1730
  searchDir(fullPath, label, baseDir);
1559
1731
  } else if (entry.name.endsWith(".jsonl")) {
1560
1732
  try {
1561
- const content = fs6.readFileSync(fullPath, "utf-8");
1733
+ const content = fs7.readFileSync(fullPath, "utf-8");
1562
1734
  const lines = content.split("\n");
1563
1735
  let found = false;
1564
1736
  for (let lineno = 0; lineno < lines.length; lineno++) {
@@ -1567,9 +1739,9 @@ function sessionCommand() {
1567
1739
  const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
1568
1740
  if (match) {
1569
1741
  if (!found) {
1570
- const relPath = path6.relative(baseDir, fullPath);
1571
- const projEnc = relPath.split(path6.sep)[0];
1572
- 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");
1573
1745
  const projName = label ? projEnc : decodePath(projEnc);
1574
1746
  console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1575
1747
  found = true;
@@ -1602,11 +1774,12 @@ function sessionCommand() {
1602
1774
  for (const { root, label } of searchRoots) {
1603
1775
  searchDir(root, label, root);
1604
1776
  }
1605
- });
1606
- 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}`);
1607
1780
  let files;
1608
1781
  try {
1609
- files = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1782
+ files = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1610
1783
  } catch {
1611
1784
  console.log("(no session files found)");
1612
1785
  return;
@@ -1620,7 +1793,7 @@ function sessionCommand() {
1620
1793
  console.log(fmt("---", "----------", "-------", "---", ""));
1621
1794
  for (const file of files) {
1622
1795
  try {
1623
- 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"));
1624
1797
  const pid = String(data.pid || "?");
1625
1798
  const sessionId = data.sessionId || "?";
1626
1799
  const cwd = data.cwd || "?";
@@ -1635,8 +1808,9 @@ function sessionCommand() {
1635
1808
  } catch {
1636
1809
  }
1637
1810
  }
1638
- });
1639
- 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)"}`);
1640
1814
  let nProjects = 0;
1641
1815
  let nSessions = 0;
1642
1816
  let totalMsgs = 0;
@@ -1646,8 +1820,8 @@ function sessionCommand() {
1646
1820
  const walk = (dir) => {
1647
1821
  const results = [];
1648
1822
  try {
1649
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1650
- 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);
1651
1825
  if (entry.isDirectory()) results.push(...walk(fullPath));
1652
1826
  else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
1653
1827
  }
@@ -1656,7 +1830,7 @@ function sessionCommand() {
1656
1830
  return results;
1657
1831
  };
1658
1832
  try {
1659
- nProjects = fs6.readdirSync(PROJECTS_DIR).length;
1833
+ nProjects = fs7.readdirSync(PROJECTS_DIR).length;
1660
1834
  } catch {
1661
1835
  }
1662
1836
  try {
@@ -1664,7 +1838,7 @@ function sessionCommand() {
1664
1838
  nSessions = sessionFiles.length;
1665
1839
  for (const f of sessionFiles) {
1666
1840
  try {
1667
- const content = fs6.readFileSync(f, "utf-8");
1841
+ const content = fs7.readFileSync(f, "utf-8");
1668
1842
  totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1669
1843
  } catch {
1670
1844
  }
@@ -1677,7 +1851,7 @@ function sessionCommand() {
1677
1851
  nDesktopSessions = desktopFiles.length;
1678
1852
  for (const f of desktopFiles) {
1679
1853
  try {
1680
- const content = fs6.readFileSync(f, "utf-8");
1854
+ const content = fs7.readFileSync(f, "utf-8");
1681
1855
  nDesktopMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1682
1856
  } catch {
1683
1857
  }
@@ -1686,7 +1860,7 @@ function sessionCommand() {
1686
1860
  }
1687
1861
  }
1688
1862
  try {
1689
- nActive = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1863
+ nActive = fs7.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1690
1864
  } catch {
1691
1865
  }
1692
1866
  console.log(`Projects: ${nProjects}`);
@@ -1709,8 +1883,9 @@ function sessionCommand() {
1709
1883
  if (desktopSize) {
1710
1884
  console.log(` Desktop: ${desktopSize}`);
1711
1885
  }
1712
- });
1713
- 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}`);
1714
1889
  const days = parseInt(opts.days, 10);
1715
1890
  const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1e3;
1716
1891
  let deleted = 0;
@@ -1718,23 +1893,23 @@ function sessionCommand() {
1718
1893
  const walk = (dir) => {
1719
1894
  let entries;
1720
1895
  try {
1721
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1896
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
1722
1897
  } catch {
1723
1898
  return;
1724
1899
  }
1725
1900
  for (const entry of entries) {
1726
- const fullPath = path6.join(dir, entry.name);
1901
+ const fullPath = path7.join(dir, entry.name);
1727
1902
  if (entry.isDirectory()) {
1728
1903
  walk(fullPath);
1729
1904
  } else if (entry.name.endsWith(".jsonl")) {
1730
1905
  try {
1731
- const stat = fs6.statSync(fullPath);
1906
+ const stat = fs7.statSync(fullPath);
1732
1907
  if (stat.mtimeMs < cutoffMs) {
1733
1908
  const size = stat.size;
1734
1909
  if (opts.dryRun) {
1735
1910
  console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
1736
1911
  } else {
1737
- fs6.unlinkSync(fullPath);
1912
+ fs7.unlinkSync(fullPath);
1738
1913
  console.log(`Deleted: ${fullPath}`);
1739
1914
  }
1740
1915
  deleted++;
@@ -1752,7 +1927,7 @@ function sessionCommand() {
1752
1927
  console.log("");
1753
1928
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1754
1929
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
1755
- });
1930
+ }));
1756
1931
  return session;
1757
1932
  }
1758
1933
 
@@ -2066,7 +2241,7 @@ var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-
2066
2241
 
2067
2242
  // src/complete/index.ts
2068
2243
  function completionCommand() {
2069
- 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) => {
2070
2245
  switch (shell) {
2071
2246
  case "zsh":
2072
2247
  process.stdout.write(ZSH_COMPLETION);
@@ -2081,12 +2256,16 @@ function completionCommand() {
2081
2256
  console.error(`Unsupported shell: ${shell}. Use 'bash', 'zsh', or 'powershell'.`);
2082
2257
  process.exit(1);
2083
2258
  }
2084
- });
2259
+ }));
2085
2260
  }
2086
2261
 
2087
2262
  // src/index.ts
2088
2263
  var _require = createRequire(import.meta.url);
2089
2264
  var { version } = _require("../package.json");
2265
+ ensureSettingsFile();
2266
+ var settings = readJson(SETTINGS_FILE);
2267
+ setLogLevel(settings._cc_hub_logLevel || "INFO");
2268
+ installGlobalExceptionHandlers();
2090
2269
  var program = new Command6();
2091
2270
  program.name("cc-hub").description("Manage Claude CLI profiles, hooks, and sessions").version(version);
2092
2271
  program.addCommand(profileCommand());
@@ -2096,4 +2275,9 @@ program.addCommand(hooksCommand());
2096
2275
  program.addCommand(sessionCommand());
2097
2276
  program.addCommand(completionCommand());
2098
2277
  program.addCommand(providerCommand());
2099
- 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.3",
3
+ "version": "1.1.5",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {