cc-hub-cli 1.0.10 → 1.1.1
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 +9 -1
- package/dist/index.js +687 -209
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Manage Claude CLI profiles, hooks, and sessions — one tool, all in one place.
|
|
4
4
|
|
|
5
|
+
> **Note:** macOS, Linux, and Windows supported.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -98,9 +100,12 @@ cc-hub hook disable -i <index> [-i <index>] # Disable active h
|
|
|
98
100
|
**Examples:**
|
|
99
101
|
|
|
100
102
|
```bash
|
|
101
|
-
# Desktop notification when Claude finishes
|
|
103
|
+
# Desktop notification when Claude finishes (macOS)
|
|
102
104
|
cc-hub hook add -e Stop -c 'osascript -e "display notification \"Done\""'
|
|
103
105
|
|
|
106
|
+
# Desktop notification when Claude finishes (Windows PowerShell)
|
|
107
|
+
cc-hub hook add -e Stop -c 'powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show(\"Claude Done\")"'
|
|
108
|
+
|
|
104
109
|
# Hook only for Bash tool usage
|
|
105
110
|
cc-hub hook add -e PreToolUse -m Bash -c 'echo "Running bash..."'
|
|
106
111
|
|
|
@@ -147,6 +152,9 @@ eval "$(cc-hub complete zsh)"
|
|
|
147
152
|
|
|
148
153
|
# bash — add to ~/.bashrc
|
|
149
154
|
eval "$(cc-hub complete bash)"
|
|
155
|
+
|
|
156
|
+
# PowerShell — add to $PROFILE
|
|
157
|
+
Invoke-Expression (& cc-hub complete powershell | Out-String)
|
|
150
158
|
```
|
|
151
159
|
|
|
152
160
|
Completes subcommands, profile names, and event types.
|
package/dist/index.js
CHANGED
|
@@ -4,31 +4,315 @@
|
|
|
4
4
|
import { Command as Command6 } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
|
|
7
|
-
// src/profiles.ts
|
|
7
|
+
// src/profiles/commands.ts
|
|
8
8
|
import { Command as Command2 } from "commander";
|
|
9
|
-
import { spawnSync, spawn } from "child_process";
|
|
10
9
|
|
|
11
10
|
// src/config.ts
|
|
11
|
+
import fs3 from "fs";
|
|
12
|
+
import path3 from "path";
|
|
13
|
+
import os2 from "os";
|
|
14
|
+
|
|
15
|
+
// src/platform/desktop-app.ts
|
|
12
16
|
import fs from "fs";
|
|
13
17
|
import path from "path";
|
|
14
18
|
import os from "os";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
function sortSemverDesc(a, b) {
|
|
20
|
+
const parse = (v) => v.split(".").map((n) => parseInt(n, 10));
|
|
21
|
+
const av = parse(a);
|
|
22
|
+
const bv = parse(b);
|
|
23
|
+
for (let i = 0; i < Math.max(av.length, bv.length); i++) {
|
|
24
|
+
const an = av[i] || 0;
|
|
25
|
+
const bn = bv[i] || 0;
|
|
26
|
+
if (an !== bn) return bn - an;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
var MacOSDesktopApp = class {
|
|
31
|
+
supportDir = path.join(os.homedir(), "Library/Application Support/Claude-3p");
|
|
32
|
+
isInstalled() {
|
|
33
|
+
return fs.existsSync(this.supportDir);
|
|
34
|
+
}
|
|
35
|
+
getSupportDir() {
|
|
36
|
+
return this.isInstalled() ? this.supportDir : void 0;
|
|
37
|
+
}
|
|
38
|
+
getSessionsDir() {
|
|
39
|
+
return this.isInstalled() ? path.join(this.supportDir, "local-agent-mode-sessions") : void 0;
|
|
40
|
+
}
|
|
41
|
+
getConfigLibrary() {
|
|
42
|
+
return this.isInstalled() ? path.join(this.supportDir, "configLibrary") : void 0;
|
|
43
|
+
}
|
|
44
|
+
findBinary() {
|
|
45
|
+
const claudeCodeDir = path.join(this.supportDir, "claude-code");
|
|
46
|
+
if (!fs.existsSync(claudeCodeDir)) return void 0;
|
|
47
|
+
let versions;
|
|
48
|
+
try {
|
|
49
|
+
versions = fs.readdirSync(claudeCodeDir).filter(
|
|
50
|
+
(d) => fs.existsSync(path.join(claudeCodeDir, d, "claude.app", "Contents", "MacOS", "claude"))
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
if (versions.length === 0) return void 0;
|
|
56
|
+
versions.sort(sortSemverDesc);
|
|
57
|
+
return path.join(claudeCodeDir, versions[0], "claude.app", "Contents", "MacOS", "claude");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var WindowsDesktopApp = class {
|
|
61
|
+
candidates = [
|
|
62
|
+
path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude"),
|
|
63
|
+
path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude-3p"),
|
|
64
|
+
path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Claude"),
|
|
65
|
+
path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Claude-3p"),
|
|
66
|
+
path.join(
|
|
67
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
|
|
68
|
+
"Packages",
|
|
69
|
+
"Claude_pzs8sxrjxfjjc",
|
|
70
|
+
"LocalCache",
|
|
71
|
+
"Roaming",
|
|
72
|
+
"Claude"
|
|
73
|
+
)
|
|
74
|
+
];
|
|
75
|
+
_findSupportDir() {
|
|
76
|
+
for (const dir of this.candidates) {
|
|
77
|
+
if (fs.existsSync(dir)) return dir;
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
isInstalled() {
|
|
82
|
+
return this._findSupportDir() !== void 0;
|
|
83
|
+
}
|
|
84
|
+
getSupportDir() {
|
|
85
|
+
return this._findSupportDir();
|
|
86
|
+
}
|
|
87
|
+
getSessionsDir() {
|
|
88
|
+
const dir = this._findSupportDir();
|
|
89
|
+
return dir ? path.join(dir, "local-agent-mode-sessions") : void 0;
|
|
90
|
+
}
|
|
91
|
+
getConfigLibrary() {
|
|
92
|
+
const dir = this._findSupportDir();
|
|
93
|
+
return dir ? path.join(dir, "configLibrary") : void 0;
|
|
94
|
+
}
|
|
95
|
+
findBinary() {
|
|
96
|
+
const win32Binary = path.join(process.env.LOCALAPPDATA || "", "Programs", "Claude", "Claude.exe");
|
|
97
|
+
if (fs.existsSync(win32Binary)) return win32Binary;
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var NoOpDesktopApp = class {
|
|
102
|
+
isInstalled() {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
getSupportDir() {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
getSessionsDir() {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
getConfigLibrary() {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
findBinary() {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/platform/profile-syncer.ts
|
|
120
|
+
import fs2 from "fs";
|
|
121
|
+
import path2 from "path";
|
|
122
|
+
import { randomUUID } from "crypto";
|
|
123
|
+
function toDesktopProfile(p) {
|
|
124
|
+
const models = p.models || (p.model ? [p.model] : []);
|
|
125
|
+
const isAnthropic = p.provider === "anthropic" || !p.provider && !p.url;
|
|
126
|
+
if (isAnthropic && !p.url) {
|
|
127
|
+
return {
|
|
128
|
+
inferenceProvider: "1p",
|
|
129
|
+
inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
inferenceProvider: "gateway",
|
|
134
|
+
inferenceGatewayBaseUrl: p.url || void 0,
|
|
135
|
+
inferenceGatewayApiKey: p.token || void 0,
|
|
136
|
+
inferenceGatewayAuthScheme: "bearer",
|
|
137
|
+
inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var DesktopProfileSyncer = class {
|
|
141
|
+
constructor(app) {
|
|
142
|
+
this.app = app;
|
|
143
|
+
}
|
|
144
|
+
app;
|
|
145
|
+
isSupported() {
|
|
146
|
+
return this.app.isInstalled();
|
|
147
|
+
}
|
|
148
|
+
sync(name, p) {
|
|
149
|
+
const configLib = this.app.getConfigLibrary();
|
|
150
|
+
if (!configLib) return;
|
|
151
|
+
if (!fs2.existsSync(configLib)) {
|
|
152
|
+
fs2.mkdirSync(configLib, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
const meta = this.readMeta();
|
|
155
|
+
const entries = meta.entries || [];
|
|
156
|
+
let id = p.desktopId;
|
|
157
|
+
if (!id) {
|
|
158
|
+
const existingByName = entries.find((e) => e.name === name);
|
|
159
|
+
if (existingByName) {
|
|
160
|
+
id = existingByName.id;
|
|
161
|
+
} else {
|
|
162
|
+
id = randomUUID();
|
|
163
|
+
}
|
|
164
|
+
p.desktopId = id;
|
|
165
|
+
}
|
|
166
|
+
const existingIndex = entries.findIndex((e) => e.id === id);
|
|
167
|
+
if (existingIndex !== -1) {
|
|
168
|
+
entries[existingIndex].name = name;
|
|
169
|
+
} else {
|
|
170
|
+
entries.push({ id, name });
|
|
171
|
+
}
|
|
172
|
+
meta.entries = entries;
|
|
173
|
+
this.writeMeta(meta);
|
|
174
|
+
this.writeProfile(id, configLib, toDesktopProfile(p));
|
|
175
|
+
}
|
|
176
|
+
remove(name, p) {
|
|
177
|
+
const configLib = this.app.getConfigLibrary();
|
|
178
|
+
if (!configLib || !p.desktopId) return;
|
|
179
|
+
const meta = this.readMeta();
|
|
180
|
+
if (meta.entries) {
|
|
181
|
+
meta.entries = meta.entries.filter((e) => e.id !== p.desktopId);
|
|
182
|
+
}
|
|
183
|
+
if (meta.appliedId === p.desktopId) {
|
|
184
|
+
delete meta.appliedId;
|
|
185
|
+
}
|
|
186
|
+
this.writeMeta(meta);
|
|
187
|
+
const filePath = path2.join(configLib, `${p.desktopId}.json`);
|
|
188
|
+
if (fs2.existsSync(filePath)) {
|
|
189
|
+
fs2.unlinkSync(filePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
setActive(p) {
|
|
193
|
+
const configLib = this.app.getConfigLibrary();
|
|
194
|
+
if (!configLib || !p.desktopId) return;
|
|
195
|
+
const meta = this.readMeta();
|
|
196
|
+
meta.appliedId = p.desktopId;
|
|
197
|
+
const entries = meta.entries || [];
|
|
198
|
+
if (!entries.some((e) => e.id === p.desktopId)) {
|
|
199
|
+
entries.push({ id: p.desktopId, name: "unknown" });
|
|
200
|
+
meta.entries = entries;
|
|
201
|
+
}
|
|
202
|
+
this.writeMeta(meta);
|
|
203
|
+
}
|
|
204
|
+
metaFile() {
|
|
205
|
+
const configLib = this.app.getConfigLibrary();
|
|
206
|
+
return configLib ? path2.join(configLib, "_meta.json") : void 0;
|
|
207
|
+
}
|
|
208
|
+
readMeta() {
|
|
209
|
+
const file = this.metaFile();
|
|
210
|
+
if (!file || !fs2.existsSync(file)) return {};
|
|
211
|
+
try {
|
|
212
|
+
return readJson(file);
|
|
213
|
+
} catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
writeMeta(meta) {
|
|
218
|
+
const file = this.metaFile();
|
|
219
|
+
if (file) writeJson(file, meta);
|
|
220
|
+
}
|
|
221
|
+
writeProfile(id, configLib, data) {
|
|
222
|
+
writeJson(path2.join(configLib, `${id}.json`), data);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/platform/binary-resolver.ts
|
|
227
|
+
import { spawnSync } from "child_process";
|
|
228
|
+
var SystemBinaryResolver = class {
|
|
229
|
+
constructor(app) {
|
|
230
|
+
this.app = app;
|
|
231
|
+
}
|
|
232
|
+
app;
|
|
233
|
+
resolve() {
|
|
234
|
+
try {
|
|
235
|
+
const result = spawnSync("claude", ["--version"], {
|
|
236
|
+
shell: process.platform === "win32",
|
|
237
|
+
encoding: "utf-8"
|
|
238
|
+
});
|
|
239
|
+
if (result.status === 0) {
|
|
240
|
+
return "claude";
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
const desktopBinary = this.app.findBinary();
|
|
245
|
+
if (desktopBinary) return desktopBinary;
|
|
246
|
+
console.error("Error: Could not find Claude Code CLI.");
|
|
247
|
+
console.error("Install it globally or install the Claude Code desktop app.");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/platform/path-codec.ts
|
|
253
|
+
var UnixPathCodec = class {
|
|
254
|
+
encode(p) {
|
|
255
|
+
return p.replace(/[\\/]/g, "-").replace(/\./g, "-").replace(/:/g, "");
|
|
256
|
+
}
|
|
257
|
+
decode(encoded) {
|
|
258
|
+
return encoded.replace(/--/g, "/.").replace(/-/g, "/");
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var WindowsPathCodec = class {
|
|
262
|
+
encode(p) {
|
|
263
|
+
return p.replace(/[\\/]/g, "-").replace(/\./g, "-").replace(/:/g, "");
|
|
264
|
+
}
|
|
265
|
+
decode(encoded) {
|
|
266
|
+
const decoded = encoded.replace(/--/g, "\\.").replace(/-/g, "\\");
|
|
267
|
+
if (/^[A-Za-z]\\/.test(decoded)) {
|
|
268
|
+
return decoded[0] + ":" + decoded.slice(1);
|
|
269
|
+
}
|
|
270
|
+
return decoded;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/platform/index.ts
|
|
275
|
+
function createDesktopApp() {
|
|
276
|
+
if (process.platform === "darwin") return new MacOSDesktopApp();
|
|
277
|
+
if (process.platform === "win32") return new WindowsDesktopApp();
|
|
278
|
+
return new NoOpDesktopApp();
|
|
279
|
+
}
|
|
280
|
+
function createProfileSyncer() {
|
|
281
|
+
return new DesktopProfileSyncer(createDesktopApp());
|
|
282
|
+
}
|
|
283
|
+
function createBinaryResolver() {
|
|
284
|
+
return new SystemBinaryResolver(createDesktopApp());
|
|
285
|
+
}
|
|
286
|
+
function createPathCodec() {
|
|
287
|
+
if (process.platform === "win32") return new WindowsPathCodec();
|
|
288
|
+
return new UnixPathCodec();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/config.ts
|
|
292
|
+
var CLAUDE_DIR = process.env.CLAUDE_DIR || path3.join(os2.homedir(), ".claude");
|
|
293
|
+
var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path3.join(CLAUDE_DIR, "profiles.json");
|
|
294
|
+
var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path3.join(CLAUDE_DIR, "settings.json");
|
|
295
|
+
var CLAUDE_JSON = path3.join(os2.homedir(), ".claude.json");
|
|
296
|
+
var PROJECTS_DIR = path3.join(CLAUDE_DIR, "projects");
|
|
297
|
+
var SESSIONS_DIR = path3.join(CLAUDE_DIR, "sessions");
|
|
298
|
+
var desktopApp = createDesktopApp();
|
|
299
|
+
var DESKTOP_CONFIG_LIBRARY = desktopApp.getConfigLibrary() || "";
|
|
300
|
+
var DESKTOP_META_FILE = DESKTOP_CONFIG_LIBRARY ? path3.join(DESKTOP_CONFIG_LIBRARY, "_meta.json") : "";
|
|
301
|
+
var DESKTOP_SESSIONS_DIR = desktopApp.getSessionsDir() || "";
|
|
302
|
+
function isDesktopAppInstalled() {
|
|
303
|
+
return desktopApp.isInstalled();
|
|
304
|
+
}
|
|
21
305
|
function ensureFile(filePath, defaultContent) {
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
|
|
306
|
+
if (!fs3.existsSync(filePath)) {
|
|
307
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
308
|
+
fs3.writeFileSync(filePath, defaultContent, "utf-8");
|
|
25
309
|
}
|
|
26
310
|
}
|
|
27
311
|
function readJson(filePath) {
|
|
28
|
-
return JSON.parse(
|
|
312
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
29
313
|
}
|
|
30
314
|
function writeJson(filePath, data) {
|
|
31
|
-
|
|
315
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
32
316
|
}
|
|
33
317
|
function ensureProfilesFile() {
|
|
34
318
|
ensureFile(PROFILES_FILE, '{"profiles":{}}\n');
|
|
@@ -37,12 +321,12 @@ function ensureSettingsFile() {
|
|
|
37
321
|
ensureFile(SETTINGS_FILE, "{}\n");
|
|
38
322
|
}
|
|
39
323
|
function fixJsonFile(filePath, fallback = {}) {
|
|
40
|
-
if (!
|
|
41
|
-
const backupPath =
|
|
42
|
-
const raw =
|
|
324
|
+
if (!fs3.existsSync(filePath)) return;
|
|
325
|
+
const backupPath = path3.join(CLAUDE_DIR, path3.basename(filePath) + ".backup");
|
|
326
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
43
327
|
try {
|
|
44
328
|
JSON.parse(raw);
|
|
45
|
-
|
|
329
|
+
fs3.copyFileSync(filePath, backupPath);
|
|
46
330
|
return;
|
|
47
331
|
} catch {
|
|
48
332
|
}
|
|
@@ -67,22 +351,26 @@ function fixJsonFile(filePath, fallback = {}) {
|
|
|
67
351
|
if (openCurly > 0) text += "}".repeat(openCurly);
|
|
68
352
|
try {
|
|
69
353
|
JSON.parse(text);
|
|
70
|
-
|
|
71
|
-
console.error(`Fixed invalid JSON in ${
|
|
354
|
+
fs3.writeFileSync(filePath, text + "\n", "utf-8");
|
|
355
|
+
console.error(`Fixed invalid JSON in ${path3.basename(filePath)}.`);
|
|
72
356
|
} catch {
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
console.error(`Restored ${
|
|
357
|
+
if (fs3.existsSync(backupPath)) {
|
|
358
|
+
fs3.copyFileSync(backupPath, filePath);
|
|
359
|
+
console.error(`Restored ${path3.basename(filePath)} from backup.`);
|
|
76
360
|
} else {
|
|
77
361
|
writeJson(filePath, fallback);
|
|
78
|
-
console.error(`Could not fix ${
|
|
362
|
+
console.error(`Could not fix ${path3.basename(filePath)}, no backup found, reset to default.`);
|
|
79
363
|
}
|
|
80
364
|
}
|
|
81
365
|
}
|
|
82
366
|
|
|
83
|
-
// src/
|
|
84
|
-
import
|
|
367
|
+
// src/profiles/runner.ts
|
|
368
|
+
import { spawnSync as spawnSync2, spawn } from "child_process";
|
|
369
|
+
|
|
370
|
+
// src/provider/index.ts
|
|
85
371
|
import { Command } from "commander";
|
|
372
|
+
|
|
373
|
+
// src/provider/transform.ts
|
|
86
374
|
function sanitizeToolId(id) {
|
|
87
375
|
let sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
88
376
|
if (!/^[a-zA-Z]/.test(sanitized)) {
|
|
@@ -293,6 +581,9 @@ data: ${JSON.stringify(data)}
|
|
|
293
581
|
});
|
|
294
582
|
yield sse("message_stop", { type: "message_stop" });
|
|
295
583
|
}
|
|
584
|
+
|
|
585
|
+
// src/provider/server.ts
|
|
586
|
+
import http from "http";
|
|
296
587
|
async function startOpenAIProxy(targetUrl, apiKey, model, models = []) {
|
|
297
588
|
const base = targetUrl.replace(/\/+$/, "");
|
|
298
589
|
const server = http.createServer(async (req, res) => {
|
|
@@ -404,6 +695,8 @@ function readBody(req) {
|
|
|
404
695
|
req.on("error", reject);
|
|
405
696
|
});
|
|
406
697
|
}
|
|
698
|
+
|
|
699
|
+
// src/provider/index.ts
|
|
407
700
|
var PROVIDERS = [
|
|
408
701
|
{
|
|
409
702
|
name: "anthropic",
|
|
@@ -427,7 +720,100 @@ function providerCommand() {
|
|
|
427
720
|
return cmd;
|
|
428
721
|
}
|
|
429
722
|
|
|
430
|
-
// src/profiles.ts
|
|
723
|
+
// src/profiles/runner.ts
|
|
724
|
+
function resolveClaudeBinary() {
|
|
725
|
+
return createBinaryResolver().resolve();
|
|
726
|
+
}
|
|
727
|
+
function updateSettingsForProfile(p) {
|
|
728
|
+
ensureSettingsFile();
|
|
729
|
+
const settings = readJson(SETTINGS_FILE);
|
|
730
|
+
const models = p.models || (p.model ? [p.model] : []);
|
|
731
|
+
delete settings.model;
|
|
732
|
+
delete settings.availableModels;
|
|
733
|
+
const envVarsToClean = [
|
|
734
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
735
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
|
736
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION",
|
|
737
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
738
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
|
739
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION",
|
|
740
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
741
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
|
742
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
|
|
743
|
+
"ANTHROPIC_CUSTOM_MODEL_OPTION"
|
|
744
|
+
];
|
|
745
|
+
if (settings.env) {
|
|
746
|
+
const env = settings.env;
|
|
747
|
+
for (const key of envVarsToClean) {
|
|
748
|
+
delete env[key];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
writeJson(SETTINGS_FILE, settings);
|
|
752
|
+
}
|
|
753
|
+
function execClaude(profileName, p, extraArgs) {
|
|
754
|
+
updateSettingsForProfile(p);
|
|
755
|
+
const models = p.models || (p.model ? [p.model] : []);
|
|
756
|
+
const firstModel = models[0];
|
|
757
|
+
const binary = resolveClaudeBinary();
|
|
758
|
+
const cmd = [binary];
|
|
759
|
+
if (firstModel) cmd.push("--model", firstModel);
|
|
760
|
+
cmd.push(...extraArgs);
|
|
761
|
+
const env = {
|
|
762
|
+
...process.env,
|
|
763
|
+
ANTHROPIC_AUTH_TOKEN: p.token || void 0,
|
|
764
|
+
ANTHROPIC_BASE_URL: p.url || void 0,
|
|
765
|
+
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
|
|
766
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1"
|
|
767
|
+
};
|
|
768
|
+
if (models.length > 0) {
|
|
769
|
+
if (models[0]) {
|
|
770
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = models[0];
|
|
771
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME = models[0];
|
|
772
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION = `Custom: ${models[0]}`;
|
|
773
|
+
}
|
|
774
|
+
if (models[1]) {
|
|
775
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = models[1];
|
|
776
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME = models[1];
|
|
777
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION = `Custom: ${models[1]}`;
|
|
778
|
+
}
|
|
779
|
+
if (models[2]) {
|
|
780
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = models[2];
|
|
781
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME = models[2];
|
|
782
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION = `Custom: ${models[2]}`;
|
|
783
|
+
}
|
|
784
|
+
env.ANTHROPIC_CUSTOM_MODEL_OPTION = models[0];
|
|
785
|
+
}
|
|
786
|
+
delete env.ANTHROPIC_API_KEY;
|
|
787
|
+
console.error(`Using profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"}`);
|
|
788
|
+
if (p.provider === "openai") {
|
|
789
|
+
const allModels = p.models || (p.model ? [p.model] : []);
|
|
790
|
+
startOpenAIProxy(
|
|
791
|
+
p.url || "https://api.openai.com",
|
|
792
|
+
p.token || "",
|
|
793
|
+
firstModel || "gpt-4o",
|
|
794
|
+
allModels
|
|
795
|
+
).then(({ baseUrl, stop }) => {
|
|
796
|
+
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
797
|
+
const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env, shell: process.platform === "win32" });
|
|
798
|
+
child.on("close", (code) => {
|
|
799
|
+
stop();
|
|
800
|
+
process.exit(code ?? 1);
|
|
801
|
+
});
|
|
802
|
+
}).catch((err) => {
|
|
803
|
+
console.error("Failed to start OpenAI proxy:", err);
|
|
804
|
+
process.exit(1);
|
|
805
|
+
});
|
|
806
|
+
} else {
|
|
807
|
+
const result = spawnSync2(cmd[0], cmd.slice(1), {
|
|
808
|
+
stdio: "inherit",
|
|
809
|
+
env,
|
|
810
|
+
shell: process.platform === "win32"
|
|
811
|
+
});
|
|
812
|
+
process.exit(result.status ?? 1);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/profiles/commands.ts
|
|
431
817
|
function maskToken(token) {
|
|
432
818
|
if (!token) return "(unset)";
|
|
433
819
|
if (token.length <= 12) return token;
|
|
@@ -463,47 +849,21 @@ function isAnthropicModel(model) {
|
|
|
463
849
|
if (lower.startsWith("claude-")) return true;
|
|
464
850
|
return false;
|
|
465
851
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
const settings = readJson(SETTINGS_FILE);
|
|
469
|
-
const models = p.models || (p.model ? [p.model] : []);
|
|
470
|
-
if (models.length > 0) {
|
|
471
|
-
const aliases = [];
|
|
472
|
-
if (models[0]) aliases.push("sonnet");
|
|
473
|
-
if (models[1]) aliases.push("opus");
|
|
474
|
-
if (models[2]) aliases.push("haiku");
|
|
475
|
-
settings.model = aliases[0];
|
|
476
|
-
settings.availableModels = aliases;
|
|
477
|
-
} else {
|
|
478
|
-
delete settings.model;
|
|
479
|
-
delete settings.availableModels;
|
|
480
|
-
}
|
|
481
|
-
const envVarsToClean = [
|
|
482
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
483
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
|
484
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION",
|
|
485
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
486
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
|
487
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION",
|
|
488
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
489
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
|
490
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION",
|
|
491
|
-
"ANTHROPIC_CUSTOM_MODEL_OPTION"
|
|
492
|
-
];
|
|
493
|
-
if (settings.env) {
|
|
494
|
-
for (const key of envVarsToClean) {
|
|
495
|
-
delete settings.env[key];
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
writeJson(SETTINGS_FILE, settings);
|
|
852
|
+
function collect(value, previous) {
|
|
853
|
+
return previous.concat([value]);
|
|
499
854
|
}
|
|
500
855
|
function profileCommand() {
|
|
501
856
|
const profile = new Command2("profile").description("Manage Claude CLI profiles");
|
|
502
|
-
|
|
857
|
+
const syncer = createProfileSyncer();
|
|
858
|
+
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) => {
|
|
859
|
+
const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
|
|
860
|
+
if (models && models.length > 3) {
|
|
861
|
+
console.error("Error: A profile can have at most 3 models.");
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
503
864
|
ensureProfilesFile();
|
|
504
865
|
const data = readJson(PROFILES_FILE);
|
|
505
866
|
const profile2 = data.profiles[name] || {};
|
|
506
|
-
const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
|
|
507
867
|
if (models) {
|
|
508
868
|
profile2.models = models;
|
|
509
869
|
profile2.model = models[0];
|
|
@@ -512,10 +872,11 @@ function profileCommand() {
|
|
|
512
872
|
if (opts.url) profile2.url = opts.url;
|
|
513
873
|
if (opts.provider) profile2.provider = opts.provider;
|
|
514
874
|
data.profiles[name] = profile2;
|
|
875
|
+
syncer.sync(name, profile2);
|
|
515
876
|
writeJson(PROFILES_FILE, data);
|
|
516
877
|
console.log(`Profile '${name}' saved.`);
|
|
517
878
|
});
|
|
518
|
-
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
|
|
879
|
+
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) => {
|
|
519
880
|
ensureProfilesFile();
|
|
520
881
|
const data = readJson(PROFILES_FILE);
|
|
521
882
|
if (!data.profiles[name]) {
|
|
@@ -564,9 +925,15 @@ function profileCommand() {
|
|
|
564
925
|
p.model = providedModels[0];
|
|
565
926
|
}
|
|
566
927
|
}
|
|
928
|
+
const finalModels = p.models || (p.model ? [p.model] : []);
|
|
929
|
+
if (finalModels.length > 3) {
|
|
930
|
+
console.error("Error: A profile can have at most 3 models.");
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
567
933
|
if (opts.token) p.token = opts.token;
|
|
568
934
|
if (opts.url) p.url = opts.url;
|
|
569
935
|
if (opts.provider) p.provider = opts.provider;
|
|
936
|
+
syncer.sync(name, p);
|
|
570
937
|
writeJson(PROFILES_FILE, data);
|
|
571
938
|
console.log(`Profile '${name}' updated.`);
|
|
572
939
|
});
|
|
@@ -586,9 +953,11 @@ function profileCommand() {
|
|
|
586
953
|
for (const name of names) {
|
|
587
954
|
const p = profiles[name];
|
|
588
955
|
const marker = name === def ? "* " : " ";
|
|
956
|
+
const desktopMarker = p.desktopId ? " [desktop]" : "";
|
|
957
|
+
const displayName = (name + desktopMarker).padEnd(20);
|
|
589
958
|
console.log(fmt(
|
|
590
959
|
marker,
|
|
591
|
-
|
|
960
|
+
displayName,
|
|
592
961
|
formatModels(p),
|
|
593
962
|
maskToken(p.token || ""),
|
|
594
963
|
p.provider || "anthropic",
|
|
@@ -605,7 +974,8 @@ function profileCommand() {
|
|
|
605
974
|
process.exit(1);
|
|
606
975
|
}
|
|
607
976
|
if (opts.json) {
|
|
608
|
-
|
|
977
|
+
const { desktopId, ...rest } = p;
|
|
978
|
+
console.log(JSON.stringify({ name, ...rest }, null, 2));
|
|
609
979
|
} else {
|
|
610
980
|
console.log(`Name: ${name}`);
|
|
611
981
|
console.log(`Model: ${p.model || "(unset)"}`);
|
|
@@ -637,6 +1007,7 @@ function profileCommand() {
|
|
|
637
1007
|
console.error(`Profile '${name}' not found.`);
|
|
638
1008
|
process.exit(1);
|
|
639
1009
|
}
|
|
1010
|
+
syncer.remove(name, data.profiles[name]);
|
|
640
1011
|
delete data.profiles[name];
|
|
641
1012
|
writeJson(PROFILES_FILE, data);
|
|
642
1013
|
console.log(`Profile '${name}' removed.`);
|
|
@@ -668,75 +1039,33 @@ function profileCommand() {
|
|
|
668
1039
|
process.exit(1);
|
|
669
1040
|
}
|
|
670
1041
|
data.default = name;
|
|
1042
|
+
syncer.setActive(data.profiles[name]);
|
|
671
1043
|
writeJson(PROFILES_FILE, data);
|
|
672
1044
|
console.log(`Default profile set to '${name}'.`);
|
|
673
1045
|
});
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
function execClaude(profileName, p, extraArgs) {
|
|
680
|
-
updateSettingsForProfile(p);
|
|
681
|
-
const models = p.models || (p.model ? [p.model] : []);
|
|
682
|
-
const firstModel = models[0];
|
|
683
|
-
const cmd = ["claude"];
|
|
684
|
-
if (firstModel) cmd.push("--model", firstModel);
|
|
685
|
-
cmd.push(...extraArgs);
|
|
686
|
-
const env = {
|
|
687
|
-
...process.env,
|
|
688
|
-
ANTHROPIC_AUTH_TOKEN: p.token || void 0,
|
|
689
|
-
ANTHROPIC_BASE_URL: p.url || void 0,
|
|
690
|
-
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
|
|
691
|
-
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1"
|
|
692
|
-
};
|
|
693
|
-
if (models.length > 0) {
|
|
694
|
-
if (models[0]) {
|
|
695
|
-
env.ANTHROPIC_DEFAULT_SONNET_MODEL = models[0];
|
|
696
|
-
env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME = models[0];
|
|
697
|
-
env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION = `Custom: ${models[0]}`;
|
|
1046
|
+
profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(() => {
|
|
1047
|
+
if (!syncer.isSupported()) {
|
|
1048
|
+
console.error("Claude desktop app is not installed.");
|
|
1049
|
+
process.exit(1);
|
|
698
1050
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1051
|
+
ensureProfilesFile();
|
|
1052
|
+
const data = readJson(PROFILES_FILE);
|
|
1053
|
+
const names = Object.keys(data.profiles);
|
|
1054
|
+
if (names.length === 0) {
|
|
1055
|
+
console.log("No profiles to sync.");
|
|
1056
|
+
return;
|
|
703
1057
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION = `Custom: ${models[2]}`;
|
|
1058
|
+
for (const name of names) {
|
|
1059
|
+
const p = data.profiles[name];
|
|
1060
|
+
syncer.sync(name, p);
|
|
708
1061
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (p.provider === "openai") {
|
|
714
|
-
const allModels = p.models || (p.model ? [p.model] : []);
|
|
715
|
-
startOpenAIProxy(
|
|
716
|
-
p.url || "https://api.openai.com",
|
|
717
|
-
p.token || "",
|
|
718
|
-
firstModel || "gpt-4o",
|
|
719
|
-
allModels
|
|
720
|
-
).then(({ baseUrl, stop }) => {
|
|
721
|
-
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
722
|
-
const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit", env });
|
|
723
|
-
child.on("close", (code) => {
|
|
724
|
-
stop();
|
|
725
|
-
process.exit(code ?? 1);
|
|
726
|
-
});
|
|
727
|
-
}).catch((err) => {
|
|
728
|
-
console.error("Failed to start OpenAI proxy:", err);
|
|
729
|
-
process.exit(1);
|
|
730
|
-
});
|
|
731
|
-
} else {
|
|
732
|
-
const result = spawnSync(cmd[0], cmd.slice(1), {
|
|
733
|
-
stdio: "inherit",
|
|
734
|
-
env
|
|
735
|
-
});
|
|
736
|
-
process.exit(result.status ?? 1);
|
|
737
|
-
}
|
|
1062
|
+
writeJson(PROFILES_FILE, data);
|
|
1063
|
+
console.log(`Synced ${names.length} profile(s) to the desktop app.`);
|
|
1064
|
+
});
|
|
1065
|
+
return profile;
|
|
738
1066
|
}
|
|
739
1067
|
function useCommand() {
|
|
1068
|
+
const syncer = createProfileSyncer();
|
|
740
1069
|
return new Command2("use").description("Set a profile as the default").argument("<name>", "Profile name").action((name) => {
|
|
741
1070
|
ensureProfilesFile();
|
|
742
1071
|
const data = readJson(PROFILES_FILE);
|
|
@@ -745,6 +1074,7 @@ function useCommand() {
|
|
|
745
1074
|
process.exit(1);
|
|
746
1075
|
}
|
|
747
1076
|
data.default = name;
|
|
1077
|
+
syncer.setActive(data.profiles[name]);
|
|
748
1078
|
writeJson(PROFILES_FILE, data);
|
|
749
1079
|
console.log(`Default profile set to '${name}'.`);
|
|
750
1080
|
});
|
|
@@ -772,7 +1102,7 @@ function runCommand() {
|
|
|
772
1102
|
});
|
|
773
1103
|
}
|
|
774
1104
|
|
|
775
|
-
// src/hooks.ts
|
|
1105
|
+
// src/hooks/commands.ts
|
|
776
1106
|
import { Command as Command3 } from "commander";
|
|
777
1107
|
function buildFlat(data) {
|
|
778
1108
|
const rows = [];
|
|
@@ -812,26 +1142,29 @@ function buildFlat(data) {
|
|
|
812
1142
|
rows.sort((a, b) => a.seq - b.seq);
|
|
813
1143
|
return rows;
|
|
814
1144
|
}
|
|
1145
|
+
function displayHookList(data) {
|
|
1146
|
+
const rows = buildFlat(data);
|
|
1147
|
+
if (rows.length === 0) {
|
|
1148
|
+
console.log("No hooks defined.");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const fmt = (idx, active, event, matcher, cmd) => `${String(idx).padEnd(4)} ${active.padEnd(2)} ${event.padEnd(22)} ${matcher.padEnd(25)} ${cmd}`;
|
|
1152
|
+
console.log(fmt(0, "", "EVENT", "MATCHER", "COMMAND").replace(/^IDX/, "IDX").replace(/^0/, "IDX"));
|
|
1153
|
+
console.log(fmt(0, "", "-----", "-------", "-------").replace(/^0/, "---"));
|
|
1154
|
+
for (let idx = 0; idx < rows.length; idx++) {
|
|
1155
|
+
const r = rows[idx];
|
|
1156
|
+
const marker = r.active ? " " : "\u2717";
|
|
1157
|
+
const matcher = r.matcher || "(any)";
|
|
1158
|
+
const cmd = r.command.length > 60 ? r.command.slice(0, 60) + "\u2026" : r.command;
|
|
1159
|
+
console.log(fmt(idx, marker, r.event, matcher, cmd));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
815
1162
|
function hooksCommand() {
|
|
816
1163
|
const hooks = new Command3("hook").description("Manage Claude Code hooks in settings.json");
|
|
817
1164
|
hooks.command("list").description("List all hooks").action(() => {
|
|
818
1165
|
ensureSettingsFile();
|
|
819
1166
|
const data = readJson(SETTINGS_FILE);
|
|
820
|
-
|
|
821
|
-
if (rows.length === 0) {
|
|
822
|
-
console.log("No hooks defined.");
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
const fmt = (idx, active, event, matcher, cmd) => `${String(idx).padEnd(4)} ${active.padEnd(2)} ${event.padEnd(22)} ${matcher.padEnd(25)} ${cmd}`;
|
|
826
|
-
console.log(fmt(0, "", "EVENT", "MATCHER", "COMMAND").replace(/^IDX/, "IDX").replace(/^0/, "IDX"));
|
|
827
|
-
console.log(fmt(0, "", "-----", "-------", "-------").replace(/^0/, "---"));
|
|
828
|
-
for (let idx = 0; idx < rows.length; idx++) {
|
|
829
|
-
const r = rows[idx];
|
|
830
|
-
const marker = r.active ? " " : "\u2717";
|
|
831
|
-
const matcher = r.matcher || "(any)";
|
|
832
|
-
const cmd = r.command.length > 60 ? r.command.slice(0, 60) + "\u2026" : r.command;
|
|
833
|
-
console.log(fmt(idx, marker, r.event, matcher, cmd));
|
|
834
|
-
}
|
|
1167
|
+
displayHookList(data);
|
|
835
1168
|
});
|
|
836
1169
|
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) => {
|
|
837
1170
|
ensureSettingsFile();
|
|
@@ -924,6 +1257,8 @@ function hooksCommand() {
|
|
|
924
1257
|
data._cc_hub_disabled = remaining;
|
|
925
1258
|
if (remaining.length === 0) delete data._cc_hub_disabled;
|
|
926
1259
|
writeJson(SETTINGS_FILE, data);
|
|
1260
|
+
console.log("");
|
|
1261
|
+
displayHookList(data);
|
|
927
1262
|
});
|
|
928
1263
|
hooks.command("disable").description("Disable one or more hooks (removes from active)").requiredOption("-i, --index <indexes...>", "Global index from 'hooks list' (repeatable)", (v, prev) => {
|
|
929
1264
|
prev = prev || [];
|
|
@@ -960,21 +1295,52 @@ function hooksCommand() {
|
|
|
960
1295
|
console.log(`Hook ${t} (${r.event}) disabled.`);
|
|
961
1296
|
}
|
|
962
1297
|
writeJson(SETTINGS_FILE, data);
|
|
1298
|
+
console.log("");
|
|
1299
|
+
displayHookList(data);
|
|
963
1300
|
});
|
|
964
1301
|
return hooks;
|
|
965
1302
|
}
|
|
966
1303
|
|
|
967
|
-
// src/sessions.ts
|
|
968
|
-
import { Command as Command4 } from "commander";
|
|
969
|
-
import fs2 from "fs";
|
|
970
|
-
import path2 from "path";
|
|
971
|
-
import { execSync } from "child_process";
|
|
1304
|
+
// src/sessions/codec.ts
|
|
972
1305
|
function encodePath(p) {
|
|
973
|
-
return
|
|
1306
|
+
return createPathCodec().encode(p);
|
|
974
1307
|
}
|
|
975
1308
|
function decodePath(encoded) {
|
|
976
|
-
return
|
|
1309
|
+
return createPathCodec().decode(encoded);
|
|
977
1310
|
}
|
|
1311
|
+
|
|
1312
|
+
// src/sessions/stats.ts
|
|
1313
|
+
import fs4 from "fs";
|
|
1314
|
+
import path4 from "path";
|
|
1315
|
+
function getDirSize(dir) {
|
|
1316
|
+
let total = 0;
|
|
1317
|
+
try {
|
|
1318
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
1319
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1320
|
+
if (entry.isDirectory()) {
|
|
1321
|
+
total += getDirSize(fullPath);
|
|
1322
|
+
} else {
|
|
1323
|
+
total += fs4.statSync(fullPath).size;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
return total;
|
|
1329
|
+
}
|
|
1330
|
+
function formatSize(bytes) {
|
|
1331
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
1332
|
+
let size = bytes;
|
|
1333
|
+
let unitIndex = 0;
|
|
1334
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
1335
|
+
size /= 1024;
|
|
1336
|
+
unitIndex++;
|
|
1337
|
+
}
|
|
1338
|
+
return `${Math.round(size * 10) / 10}${units[unitIndex]}`;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/sessions/utils.ts
|
|
1342
|
+
import fs5 from "fs";
|
|
1343
|
+
import path5 from "path";
|
|
978
1344
|
function formatTimestamp(ms) {
|
|
979
1345
|
const d = new Date(ms);
|
|
980
1346
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -982,9 +1348,9 @@ function formatTimestamp(ms) {
|
|
|
982
1348
|
}
|
|
983
1349
|
function findProjectDir(query) {
|
|
984
1350
|
const encoded = encodePath(query);
|
|
985
|
-
if (
|
|
1351
|
+
if (fs5.existsSync(path5.join(PROJECTS_DIR, encoded))) return encoded;
|
|
986
1352
|
try {
|
|
987
|
-
const dirs =
|
|
1353
|
+
const dirs = fs5.readdirSync(PROJECTS_DIR);
|
|
988
1354
|
const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
|
|
989
1355
|
return match || null;
|
|
990
1356
|
} catch {
|
|
@@ -996,7 +1362,7 @@ function parseSessionMeta(filePath) {
|
|
|
996
1362
|
let slug = "";
|
|
997
1363
|
let customTitle = "";
|
|
998
1364
|
try {
|
|
999
|
-
const lines =
|
|
1365
|
+
const lines = fs5.readFileSync(filePath, "utf-8").split("\n");
|
|
1000
1366
|
for (const line of lines) {
|
|
1001
1367
|
if (!line.trim()) continue;
|
|
1002
1368
|
try {
|
|
@@ -1048,32 +1414,39 @@ function snippet(text, query, width = 150) {
|
|
|
1048
1414
|
const suffix = end < text.length ? "..." : "";
|
|
1049
1415
|
return prefix + text.slice(start, end) + suffix;
|
|
1050
1416
|
}
|
|
1417
|
+
|
|
1418
|
+
// src/sessions/commands.ts
|
|
1419
|
+
import { Command as Command4 } from "commander";
|
|
1420
|
+
import fs6 from "fs";
|
|
1421
|
+
import path6 from "path";
|
|
1051
1422
|
function sessionCommand() {
|
|
1052
1423
|
const session = new Command4("session").description("Manage Claude Code sessions");
|
|
1424
|
+
const desktopApp2 = createDesktopApp();
|
|
1425
|
+
const DESKTOP_SESSIONS_DIR2 = desktopApp2.getSessionsDir() || "";
|
|
1053
1426
|
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) => {
|
|
1054
1427
|
const limit = parseInt(opts.limit, 10);
|
|
1055
1428
|
let dirs;
|
|
1056
1429
|
try {
|
|
1057
|
-
dirs =
|
|
1430
|
+
dirs = fs6.readdirSync(PROJECTS_DIR);
|
|
1058
1431
|
} catch {
|
|
1059
1432
|
console.log("No projects directory found.");
|
|
1060
1433
|
return;
|
|
1061
1434
|
}
|
|
1062
1435
|
dirs.sort((a, b) => {
|
|
1063
|
-
const statA =
|
|
1064
|
-
const statB =
|
|
1436
|
+
const statA = fs6.statSync(path6.join(PROJECTS_DIR, a));
|
|
1437
|
+
const statB = fs6.statSync(path6.join(PROJECTS_DIR, b));
|
|
1065
1438
|
return statB.mtimeMs - statA.mtimeMs;
|
|
1066
1439
|
});
|
|
1067
1440
|
let count = 0;
|
|
1068
1441
|
for (const projDir of dirs) {
|
|
1069
1442
|
if (count >= limit) break;
|
|
1070
|
-
const fullPath =
|
|
1443
|
+
const fullPath = path6.join(PROJECTS_DIR, projDir);
|
|
1071
1444
|
let nSessions = 0;
|
|
1072
1445
|
try {
|
|
1073
|
-
nSessions =
|
|
1446
|
+
nSessions = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
|
|
1074
1447
|
} catch {
|
|
1075
1448
|
}
|
|
1076
|
-
const stat =
|
|
1449
|
+
const stat = fs6.statSync(fullPath);
|
|
1077
1450
|
const decoded = decodePath(projDir);
|
|
1078
1451
|
if (opts.json) {
|
|
1079
1452
|
console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
|
|
@@ -1091,7 +1464,7 @@ function sessionCommand() {
|
|
|
1091
1464
|
console.error(`No project matched: ${project}`);
|
|
1092
1465
|
process.exit(1);
|
|
1093
1466
|
}
|
|
1094
|
-
const fullPath =
|
|
1467
|
+
const fullPath = path6.join(PROJECTS_DIR, projDir);
|
|
1095
1468
|
console.log(`Project: ${decodePath(projDir)}`);
|
|
1096
1469
|
console.log(`Dir: ${fullPath}`);
|
|
1097
1470
|
console.log("");
|
|
@@ -1100,16 +1473,16 @@ function sessionCommand() {
|
|
|
1100
1473
|
console.log(fmt("----------", "----", "-------", "--------"));
|
|
1101
1474
|
let files;
|
|
1102
1475
|
try {
|
|
1103
|
-
files =
|
|
1476
|
+
files = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
|
|
1104
1477
|
} catch {
|
|
1105
1478
|
return;
|
|
1106
1479
|
}
|
|
1107
1480
|
for (const file of files) {
|
|
1108
|
-
const filePath =
|
|
1481
|
+
const filePath = path6.join(fullPath, file);
|
|
1109
1482
|
const sessionId = file.replace(/\.jsonl$/, "");
|
|
1110
1483
|
let msgCount = 0;
|
|
1111
1484
|
try {
|
|
1112
|
-
const content =
|
|
1485
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
1113
1486
|
msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
|
|
1114
1487
|
} catch {
|
|
1115
1488
|
}
|
|
@@ -1117,7 +1490,7 @@ function sessionCommand() {
|
|
|
1117
1490
|
console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
|
|
1118
1491
|
if (opts.verbose) {
|
|
1119
1492
|
try {
|
|
1120
|
-
const lines =
|
|
1493
|
+
const lines = fs6.readFileSync(filePath, "utf-8").split("\n");
|
|
1121
1494
|
for (const line of lines) {
|
|
1122
1495
|
if (!line.trim()) continue;
|
|
1123
1496
|
try {
|
|
@@ -1145,33 +1518,36 @@ function sessionCommand() {
|
|
|
1145
1518
|
}
|
|
1146
1519
|
});
|
|
1147
1520
|
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) => {
|
|
1148
|
-
let
|
|
1521
|
+
let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
|
|
1522
|
+
if (isDesktopAppInstalled()) {
|
|
1523
|
+
searchRoots.push({ root: DESKTOP_SESSIONS_DIR2, label: "[desktop] " });
|
|
1524
|
+
}
|
|
1149
1525
|
if (opts.project) {
|
|
1150
1526
|
const projDir = findProjectDir(opts.project);
|
|
1151
1527
|
if (!projDir) {
|
|
1152
1528
|
console.error(`No project matched: ${opts.project}`);
|
|
1153
1529
|
process.exit(1);
|
|
1154
1530
|
}
|
|
1155
|
-
|
|
1531
|
+
searchRoots = [{ root: path6.join(PROJECTS_DIR, projDir), label: "" }];
|
|
1156
1532
|
}
|
|
1157
1533
|
const limit = parseInt(opts.limit, 10);
|
|
1158
1534
|
let count = 0;
|
|
1159
|
-
function searchDir(dir) {
|
|
1535
|
+
function searchDir(dir, label, baseDir) {
|
|
1160
1536
|
if (count >= limit) return;
|
|
1161
1537
|
let entries;
|
|
1162
1538
|
try {
|
|
1163
|
-
entries =
|
|
1539
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1164
1540
|
} catch {
|
|
1165
1541
|
return;
|
|
1166
1542
|
}
|
|
1167
1543
|
for (const entry of entries) {
|
|
1168
1544
|
if (count >= limit) break;
|
|
1169
|
-
const fullPath =
|
|
1545
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1170
1546
|
if (entry.isDirectory()) {
|
|
1171
|
-
searchDir(fullPath);
|
|
1547
|
+
searchDir(fullPath, label, baseDir);
|
|
1172
1548
|
} else if (entry.name.endsWith(".jsonl")) {
|
|
1173
1549
|
try {
|
|
1174
|
-
const content =
|
|
1550
|
+
const content = fs6.readFileSync(fullPath, "utf-8");
|
|
1175
1551
|
const lines = content.split("\n");
|
|
1176
1552
|
let found = false;
|
|
1177
1553
|
for (let lineno = 0; lineno < lines.length; lineno++) {
|
|
@@ -1180,10 +1556,11 @@ function sessionCommand() {
|
|
|
1180
1556
|
const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
|
|
1181
1557
|
if (match) {
|
|
1182
1558
|
if (!found) {
|
|
1183
|
-
const relPath =
|
|
1184
|
-
const projEnc = relPath.split(
|
|
1185
|
-
const sessionId =
|
|
1186
|
-
|
|
1559
|
+
const relPath = path6.relative(baseDir, fullPath);
|
|
1560
|
+
const projEnc = relPath.split(path6.sep)[0];
|
|
1561
|
+
const sessionId = path6.basename(fullPath, ".jsonl");
|
|
1562
|
+
const projName = label ? projEnc : decodePath(projEnc);
|
|
1563
|
+
console.log(`${label}[${projName} \u2192 ${sessionId}]`);
|
|
1187
1564
|
found = true;
|
|
1188
1565
|
count++;
|
|
1189
1566
|
}
|
|
@@ -1211,12 +1588,14 @@ function sessionCommand() {
|
|
|
1211
1588
|
}
|
|
1212
1589
|
}
|
|
1213
1590
|
}
|
|
1214
|
-
|
|
1591
|
+
for (const { root, label } of searchRoots) {
|
|
1592
|
+
searchDir(root, label, root);
|
|
1593
|
+
}
|
|
1215
1594
|
});
|
|
1216
1595
|
session.command("ps").description("Show active Claude Code processes").action(() => {
|
|
1217
1596
|
let files;
|
|
1218
1597
|
try {
|
|
1219
|
-
files =
|
|
1598
|
+
files = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
1220
1599
|
} catch {
|
|
1221
1600
|
console.log("(no session files found)");
|
|
1222
1601
|
return;
|
|
@@ -1230,7 +1609,7 @@ function sessionCommand() {
|
|
|
1230
1609
|
console.log(fmt("---", "----------", "-------", "---", ""));
|
|
1231
1610
|
for (const file of files) {
|
|
1232
1611
|
try {
|
|
1233
|
-
const data = JSON.parse(
|
|
1612
|
+
const data = JSON.parse(fs6.readFileSync(path6.join(SESSIONS_DIR, file), "utf-8"));
|
|
1234
1613
|
const pid = String(data.pid || "?");
|
|
1235
1614
|
const sessionId = data.sessionId || "?";
|
|
1236
1615
|
const cwd = data.cwd || "?";
|
|
@@ -1251,50 +1630,73 @@ function sessionCommand() {
|
|
|
1251
1630
|
let nSessions = 0;
|
|
1252
1631
|
let totalMsgs = 0;
|
|
1253
1632
|
let nActive = 0;
|
|
1633
|
+
let nDesktopSessions = 0;
|
|
1634
|
+
let nDesktopMsgs = 0;
|
|
1635
|
+
const walk = (dir) => {
|
|
1636
|
+
const results = [];
|
|
1637
|
+
try {
|
|
1638
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
1639
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1640
|
+
if (entry.isDirectory()) results.push(...walk(fullPath));
|
|
1641
|
+
else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
|
|
1642
|
+
}
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
return results;
|
|
1646
|
+
};
|
|
1254
1647
|
try {
|
|
1255
|
-
nProjects =
|
|
1648
|
+
nProjects = fs6.readdirSync(PROJECTS_DIR).length;
|
|
1256
1649
|
} catch {
|
|
1257
1650
|
}
|
|
1258
1651
|
try {
|
|
1259
|
-
const walk = (dir) => {
|
|
1260
|
-
const results = [];
|
|
1261
|
-
try {
|
|
1262
|
-
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
1263
|
-
const fullPath = path2.join(dir, entry.name);
|
|
1264
|
-
if (entry.isDirectory()) results.push(...walk(fullPath));
|
|
1265
|
-
else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
|
|
1266
|
-
}
|
|
1267
|
-
} catch {
|
|
1268
|
-
}
|
|
1269
|
-
return results;
|
|
1270
|
-
};
|
|
1271
1652
|
const sessionFiles = walk(PROJECTS_DIR);
|
|
1272
1653
|
nSessions = sessionFiles.length;
|
|
1273
1654
|
for (const f of sessionFiles) {
|
|
1274
1655
|
try {
|
|
1275
|
-
const content =
|
|
1656
|
+
const content = fs6.readFileSync(f, "utf-8");
|
|
1276
1657
|
totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
|
|
1277
1658
|
} catch {
|
|
1278
1659
|
}
|
|
1279
1660
|
}
|
|
1280
1661
|
} catch {
|
|
1281
1662
|
}
|
|
1663
|
+
if (isDesktopAppInstalled()) {
|
|
1664
|
+
try {
|
|
1665
|
+
const desktopFiles = walk(DESKTOP_SESSIONS_DIR2);
|
|
1666
|
+
nDesktopSessions = desktopFiles.length;
|
|
1667
|
+
for (const f of desktopFiles) {
|
|
1668
|
+
try {
|
|
1669
|
+
const content = fs6.readFileSync(f, "utf-8");
|
|
1670
|
+
nDesktopMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
|
|
1671
|
+
} catch {
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
} catch {
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1282
1677
|
try {
|
|
1283
|
-
nActive =
|
|
1678
|
+
nActive = fs6.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
|
|
1284
1679
|
} catch {
|
|
1285
1680
|
}
|
|
1286
1681
|
console.log(`Projects: ${nProjects}`);
|
|
1287
|
-
console.log(`Sessions: ${nSessions}`);
|
|
1288
|
-
|
|
1682
|
+
console.log(`Sessions: ${nSessions} (CLI)`);
|
|
1683
|
+
if (isDesktopAppInstalled()) {
|
|
1684
|
+
console.log(` ${nDesktopSessions} (desktop)`);
|
|
1685
|
+
}
|
|
1686
|
+
console.log(`Total messages: ${totalMsgs} (CLI)`);
|
|
1687
|
+
if (isDesktopAppInstalled()) {
|
|
1688
|
+
console.log(` ${nDesktopMsgs} (desktop)`);
|
|
1689
|
+
}
|
|
1289
1690
|
console.log(`Active procs: ${nActive} (in ${SESSIONS_DIR})`);
|
|
1290
1691
|
console.log("");
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1692
|
+
const totalSize = formatSize(getDirSize(CLAUDE_DIR));
|
|
1693
|
+
const projSize = formatSize(getDirSize(PROJECTS_DIR));
|
|
1694
|
+
const desktopSize = isDesktopAppInstalled() ? formatSize(getDirSize(DESKTOP_SESSIONS_DIR2)) : null;
|
|
1695
|
+
console.log("Storage:");
|
|
1696
|
+
console.log(` Total: ${totalSize}`);
|
|
1697
|
+
console.log(` Projects: ${projSize}`);
|
|
1698
|
+
if (desktopSize) {
|
|
1699
|
+
console.log(` Desktop: ${desktopSize}`);
|
|
1298
1700
|
}
|
|
1299
1701
|
});
|
|
1300
1702
|
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) => {
|
|
@@ -1305,23 +1707,23 @@ function sessionCommand() {
|
|
|
1305
1707
|
const walk = (dir) => {
|
|
1306
1708
|
let entries;
|
|
1307
1709
|
try {
|
|
1308
|
-
entries =
|
|
1710
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1309
1711
|
} catch {
|
|
1310
1712
|
return;
|
|
1311
1713
|
}
|
|
1312
1714
|
for (const entry of entries) {
|
|
1313
|
-
const fullPath =
|
|
1715
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1314
1716
|
if (entry.isDirectory()) {
|
|
1315
1717
|
walk(fullPath);
|
|
1316
1718
|
} else if (entry.name.endsWith(".jsonl")) {
|
|
1317
1719
|
try {
|
|
1318
|
-
const stat =
|
|
1720
|
+
const stat = fs6.statSync(fullPath);
|
|
1319
1721
|
if (stat.mtimeMs < cutoffMs) {
|
|
1320
1722
|
const size = stat.size;
|
|
1321
1723
|
if (opts.dryRun) {
|
|
1322
1724
|
console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
|
|
1323
1725
|
} else {
|
|
1324
|
-
|
|
1726
|
+
fs6.unlinkSync(fullPath);
|
|
1325
1727
|
console.log(`Deleted: ${fullPath}`);
|
|
1326
1728
|
}
|
|
1327
1729
|
deleted++;
|
|
@@ -1333,6 +1735,9 @@ function sessionCommand() {
|
|
|
1333
1735
|
}
|
|
1334
1736
|
};
|
|
1335
1737
|
walk(PROJECTS_DIR);
|
|
1738
|
+
if (isDesktopAppInstalled()) {
|
|
1739
|
+
walk(DESKTOP_SESSIONS_DIR2);
|
|
1740
|
+
}
|
|
1336
1741
|
console.log("");
|
|
1337
1742
|
const verb = opts.dryRun ? "Would delete" : "Deleted";
|
|
1338
1743
|
console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
|
|
@@ -1340,8 +1745,10 @@ function sessionCommand() {
|
|
|
1340
1745
|
return session;
|
|
1341
1746
|
}
|
|
1342
1747
|
|
|
1343
|
-
// src/complete.ts
|
|
1748
|
+
// src/complete/index.ts
|
|
1344
1749
|
import { Command as Command5 } from "commander";
|
|
1750
|
+
|
|
1751
|
+
// src/complete/zsh.ts
|
|
1345
1752
|
var ZSH_COMPLETION = `#compdef cc-hub
|
|
1346
1753
|
|
|
1347
1754
|
_cc-hub() {
|
|
@@ -1372,6 +1779,7 @@ _cc-hub() {
|
|
|
1372
1779
|
'remove:Remove a profile'
|
|
1373
1780
|
'rename:Rename a profile'
|
|
1374
1781
|
'default:Set the default profile'
|
|
1782
|
+
'sync:Synchronize all CLI profiles to the desktop app'
|
|
1375
1783
|
)
|
|
1376
1784
|
|
|
1377
1785
|
local -a hooks_subcmds
|
|
@@ -1437,7 +1845,13 @@ _cc-hub() {
|
|
|
1437
1845
|
else
|
|
1438
1846
|
words=("stub" $words[3,-1])
|
|
1439
1847
|
(( CURRENT-- ))
|
|
1440
|
-
_arguments -C -S
|
|
1848
|
+
_arguments -C -S \\
|
|
1849
|
+
'1:profile:_cc_hub_profiles' \\
|
|
1850
|
+
'(-m --model)*'{-m,--model}'[Model ID]:model:->profileModel' \\
|
|
1851
|
+
'(-d --delete-model)*'{-d,--delete-model}'[Remove model ID]:model:->profileModel' \\
|
|
1852
|
+
'(-t --token)'{-t,--token}'[API key / token]:token:' \\
|
|
1853
|
+
'(-u --url)'{-u,--url}'[Base URL]:url:' \\
|
|
1854
|
+
'(-p --provider)'{-p,--provider}'[Provider type]:provider:(anthropic openai)'
|
|
1441
1855
|
case $state in
|
|
1442
1856
|
profileModel)
|
|
1443
1857
|
_cc_hub_models_for_profile $line[1]
|
|
@@ -1471,6 +1885,8 @@ _cc-hub() {
|
|
|
1471
1885
|
|
|
1472
1886
|
compdef _cc-hub cc-hub
|
|
1473
1887
|
`;
|
|
1888
|
+
|
|
1889
|
+
// src/complete/bash.ts
|
|
1474
1890
|
var BASH_COMPLETION = `_cc-hub_profiles() {
|
|
1475
1891
|
local profiles_file="\${CLAUDE_PROFILES_FILE:-$HOME/.claude/profiles.json}"
|
|
1476
1892
|
if [[ -f "$profiles_file" ]]; then
|
|
@@ -1515,7 +1931,7 @@ _cc-hub() {
|
|
|
1515
1931
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1516
1932
|
commands="profile use run hook session provider complete help"
|
|
1517
1933
|
|
|
1518
|
-
local profile_subcmds="add update list view remove rename default"
|
|
1934
|
+
local profile_subcmds="add update list view remove rename default sync"
|
|
1519
1935
|
local provider_subcmds="list"
|
|
1520
1936
|
local provider_types="anthropic openai"
|
|
1521
1937
|
local hooks_subcmds="list add remove enable disable"
|
|
@@ -1579,8 +1995,67 @@ _cc-hub() {
|
|
|
1579
1995
|
|
|
1580
1996
|
complete -F _cc-hub cc-hub
|
|
1581
1997
|
`;
|
|
1998
|
+
|
|
1999
|
+
// src/complete/powershell.ts
|
|
2000
|
+
var POWERSHELL_COMPLETION = `Register-ArgumentCompleter -Native -CommandName cc-hub -ScriptBlock {
|
|
2001
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
2002
|
+
|
|
2003
|
+
$commands = @(
|
|
2004
|
+
'profile:Manage Claude CLI profiles'
|
|
2005
|
+
'use:Set a profile as the default'
|
|
2006
|
+
'run:Launch Claude Code using the default or a specified profile'
|
|
2007
|
+
'hook:Manage Claude Code hooks in settings.json'
|
|
2008
|
+
'session:Manage Claude Code sessions'
|
|
2009
|
+
'provider:Manage provider types'
|
|
2010
|
+
'complete:Print shell completion functions'
|
|
2011
|
+
'help:Display help for a command'
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
$profileSubcmds = @('add', 'update', 'list', 'view', 'remove', 'rename', 'default', 'sync')
|
|
2015
|
+
$hookSubcmds = @('list', 'add', 'remove', 'enable', 'disable')
|
|
2016
|
+
$sessionSubcmds = @('list', 'show', 'search', 'ps', 'stats', 'clean')
|
|
2017
|
+
$providerSubcmds = @('list')
|
|
2018
|
+
|
|
2019
|
+
$tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
|
|
2020
|
+
|
|
2021
|
+
if ($tokens.Count -eq 1 -or ($tokens.Count -eq 2 -and $wordToComplete -ne '')) {
|
|
2022
|
+
$commands | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
|
|
2023
|
+
return
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
$cmd = $tokens[1]
|
|
2027
|
+
|
|
2028
|
+
switch ($cmd) {
|
|
2029
|
+
'profile' {
|
|
2030
|
+
if ($tokens.Count -eq 2 -or ($tokens.Count -eq 3 -and $wordToComplete -ne '')) {
|
|
2031
|
+
$profileSubcmds | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
|
|
2032
|
+
return
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
'hook' {
|
|
2036
|
+
if ($tokens.Count -eq 2 -or ($tokens.Count -eq 3 -and $wordToComplete -ne '')) {
|
|
2037
|
+
$hookSubcmds | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
|
|
2038
|
+
return
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
'session' {
|
|
2042
|
+
if ($tokens.Count -eq 2 -or ($tokens.Count -eq 3 -and $wordToComplete -ne '')) {
|
|
2043
|
+
$sessionSubcmds | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
|
|
2044
|
+
return
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
'provider' {
|
|
2048
|
+
if ($tokens.Count -eq 2 -or ($tokens.Count -eq 3 -and $wordToComplete -ne '')) {
|
|
2049
|
+
$providerSubcmds | ForEach-Object { if ($_ -like "$wordToComplete*") { $_ } }
|
|
2050
|
+
return
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}`;
|
|
2055
|
+
|
|
2056
|
+
// src/complete/index.ts
|
|
1582
2057
|
function completeCommand() {
|
|
1583
|
-
return new Command5("complete").description("Print shell completion script").argument("<shell>", "Shell type: bash or
|
|
2058
|
+
return new Command5("complete").description("Print shell completion script").argument("<shell>", "Shell type: bash, zsh, or powershell").action((shell) => {
|
|
1584
2059
|
switch (shell) {
|
|
1585
2060
|
case "zsh":
|
|
1586
2061
|
process.stdout.write(ZSH_COMPLETION);
|
|
@@ -1588,8 +2063,11 @@ function completeCommand() {
|
|
|
1588
2063
|
case "bash":
|
|
1589
2064
|
process.stdout.write(BASH_COMPLETION);
|
|
1590
2065
|
break;
|
|
2066
|
+
case "powershell":
|
|
2067
|
+
process.stdout.write(POWERSHELL_COMPLETION);
|
|
2068
|
+
break;
|
|
1591
2069
|
default:
|
|
1592
|
-
console.error(`Unsupported shell: ${shell}. Use 'bash' or '
|
|
2070
|
+
console.error(`Unsupported shell: ${shell}. Use 'bash', 'zsh', or 'powershell'.`);
|
|
1593
2071
|
process.exit(1);
|
|
1594
2072
|
}
|
|
1595
2073
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-hub-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Manage Claude CLI profiles, hooks, and sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"commander": "^13.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.6.0",
|
|
36
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
35
37
|
"tsup": "^8.4.0",
|
|
36
38
|
"typescript": "^5.7.0",
|
|
37
|
-
"vitest": "^3.0.0"
|
|
38
|
-
"@vitest/coverage-v8": "^3.0.0"
|
|
39
|
+
"vitest": "^3.0.0"
|
|
39
40
|
},
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": ">=18"
|