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.
- package/dist/index.js +380 -196
- 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
|
|
12
|
-
import
|
|
13
|
-
import
|
|
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 =
|
|
127
|
+
supportDir = path2.join(os2.homedir(), "Library/Application Support/Claude-3p");
|
|
32
128
|
isInstalled() {
|
|
33
|
-
return
|
|
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() ?
|
|
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;
|
|
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
|
-
|
|
46
|
-
|
|
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 =
|
|
50
|
-
(d) =>
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 =
|
|
69
|
-
process.env.LOCALAPPDATA ||
|
|
171
|
+
const packagesDir = path2.join(
|
|
172
|
+
process.env.LOCALAPPDATA || path2.join(os2.homedir(), "AppData", "Local"),
|
|
70
173
|
"Packages"
|
|
71
174
|
);
|
|
72
|
-
if (
|
|
175
|
+
if (fs2.existsSync(packagesDir)) {
|
|
73
176
|
try {
|
|
74
|
-
const entries =
|
|
177
|
+
const entries = fs2.readdirSync(packagesDir);
|
|
75
178
|
for (const entry of entries) {
|
|
76
179
|
if (entry.startsWith("Claude_")) {
|
|
77
|
-
candidates.push(
|
|
78
|
-
candidates.push(
|
|
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 (
|
|
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
|
-
|
|
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 ?
|
|
216
|
+
return dir ? path2.join(dir, "configLibrary") : void 0;
|
|
101
217
|
}
|
|
102
|
-
|
|
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 ?
|
|
227
|
+
return dir ? path2.join(dir, "local-agent-mode-sessions") : void 0;
|
|
105
228
|
}
|
|
106
229
|
findBinary() {
|
|
107
|
-
const win32Binary =
|
|
108
|
-
if (
|
|
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
|
|
132
|
-
import
|
|
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 (!
|
|
163
|
-
|
|
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 =
|
|
199
|
-
if (
|
|
200
|
-
|
|
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 ?
|
|
344
|
+
return configLib ? path3.join(configLib, "_meta.json") : void 0;
|
|
218
345
|
}
|
|
219
346
|
readMeta() {
|
|
220
347
|
const file = this.metaFile();
|
|
221
|
-
if (!file || !
|
|
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(
|
|
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)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 ||
|
|
304
|
-
var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE ||
|
|
305
|
-
var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE ||
|
|
306
|
-
var CLAUDE_JSON =
|
|
307
|
-
var PROJECTS_DIR =
|
|
308
|
-
var SESSIONS_DIR =
|
|
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 ?
|
|
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 (!
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
455
|
+
debug(`readJson: ${filePath}`);
|
|
456
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
324
457
|
}
|
|
325
458
|
function writeJson(filePath, data) {
|
|
326
|
-
|
|
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 (!
|
|
336
|
-
const backupPath =
|
|
337
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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 (
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
|
889
|
+
const settings2 = readJson(SETTINGS_FILE);
|
|
741
890
|
const models = p.models || (p.model ? [p.model] : []);
|
|
742
|
-
delete
|
|
743
|
-
delete
|
|
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 (
|
|
757
|
-
const 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
process.exit(1);
|
|
1184
|
+
throw new Error(`Profile '${oldName}' not found.`);
|
|
1032
1185
|
}
|
|
1033
1186
|
if (data.profiles[newName]) {
|
|
1034
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1479
|
+
const encoded = createPathCodec().encode(p);
|
|
1480
|
+
debug(`codec: encode "${p}" -> "${encoded}"`);
|
|
1481
|
+
return encoded;
|
|
1318
1482
|
}
|
|
1319
1483
|
function decodePath(encoded) {
|
|
1320
|
-
|
|
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
|
|
1325
|
-
import
|
|
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
|
|
1330
|
-
const fullPath =
|
|
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 +=
|
|
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
|
|
1354
|
-
import
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
1432
|
-
import
|
|
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 =
|
|
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 =
|
|
1448
|
-
const statB =
|
|
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 =
|
|
1626
|
+
const fullPath = path7.join(PROJECTS_DIR, projDir);
|
|
1455
1627
|
let nSessions = 0;
|
|
1456
1628
|
try {
|
|
1457
|
-
nSessions =
|
|
1629
|
+
nSessions = fs7.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
|
|
1458
1630
|
} catch {
|
|
1459
1631
|
}
|
|
1460
|
-
const stat =
|
|
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
|
-
|
|
1476
|
-
process.exit(1);
|
|
1648
|
+
throw new Error(`No project matched: ${project}`);
|
|
1477
1649
|
}
|
|
1478
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1540
|
-
process.exit(1);
|
|
1712
|
+
throw new Error(`No project matched: ${opts.project}`);
|
|
1541
1713
|
}
|
|
1542
|
-
searchRoots = [{ root:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1571
|
-
const projEnc = relPath.split(
|
|
1572
|
-
const sessionId =
|
|
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 =
|
|
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(
|
|
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
|
|
1650
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|