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