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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +687 -209
  3. 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
- var CLAUDE_DIR = process.env.CLAUDE_DIR || path.join(os.homedir(), ".claude");
16
- var PROFILES_FILE = process.env.CLAUDE_PROFILES_FILE || path.join(CLAUDE_DIR, "profiles.json");
17
- var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path.join(CLAUDE_DIR, "settings.json");
18
- var CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
19
- var PROJECTS_DIR = path.join(CLAUDE_DIR, "projects");
20
- var SESSIONS_DIR = path.join(CLAUDE_DIR, "sessions");
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 (!fs.existsSync(filePath)) {
23
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
- fs.writeFileSync(filePath, defaultContent, "utf-8");
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(fs.readFileSync(filePath, "utf-8"));
312
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
29
313
  }
30
314
  function writeJson(filePath, data) {
31
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
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 (!fs.existsSync(filePath)) return;
41
- const backupPath = path.join(CLAUDE_DIR, path.basename(filePath) + ".backup");
42
- const raw = fs.readFileSync(filePath, "utf-8");
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
- fs.copyFileSync(filePath, backupPath);
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
- fs.writeFileSync(filePath, text + "\n", "utf-8");
71
- console.error(`Fixed invalid JSON in ${path.basename(filePath)}.`);
354
+ fs3.writeFileSync(filePath, text + "\n", "utf-8");
355
+ console.error(`Fixed invalid JSON in ${path3.basename(filePath)}.`);
72
356
  } catch {
73
- if (fs.existsSync(backupPath)) {
74
- fs.copyFileSync(backupPath, filePath);
75
- console.error(`Restored ${path.basename(filePath)} from backup.`);
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 ${path.basename(filePath)}, no backup found, reset to default.`);
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/provider.ts
84
- import http from "http";
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 updateSettingsForProfile(p) {
467
- ensureSettingsFile();
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
- profile.command("add").description("Add or update a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID (e.g. claude-opus-4-6) - can be used multiple times", collect, []).option("-t, --token <token>", "API key / token").option("-u, --url <url>", "Base URL (e.g. https://api.anthropic.com)").option("-p, --provider <provider>", "Provider type: anthropic (default) or openai").action((name, opts) => {
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 to set multiple models", 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: anthropic (default) or openai").action((name, opts) => {
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
- name,
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
- console.log(JSON.stringify({ name, ...p }, null, 2));
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
- return profile;
675
- }
676
- function collect(value, previous) {
677
- return previous.concat([value]);
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
- if (models[1]) {
700
- env.ANTHROPIC_DEFAULT_OPUS_MODEL = models[1];
701
- env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME = models[1];
702
- env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION = `Custom: ${models[1]}`;
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
- if (models[2]) {
705
- env.ANTHROPIC_DEFAULT_HAIKU_MODEL = models[2];
706
- env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME = models[2];
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
- env.ANTHROPIC_CUSTOM_MODEL_OPTION = models[0];
710
- }
711
- delete env.ANTHROPIC_API_KEY;
712
- console.error(`Using profile '${profileName}': model=${firstModel || "(default)"} url=${p.url || "(default)"} provider=${p.provider || "anthropic"}`);
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
- const rows = buildFlat(data);
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 p.replace(/\./g, "DOTMARK").replace(/\//g, "-").replace(/DOTMARK/g, "-");
1306
+ return createPathCodec().encode(p);
974
1307
  }
975
1308
  function decodePath(encoded) {
976
- return encoded.replace(/--/g, "/.").replace(/-/g, "/");
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 (fs2.existsSync(path2.join(PROJECTS_DIR, encoded))) return encoded;
1351
+ if (fs5.existsSync(path5.join(PROJECTS_DIR, encoded))) return encoded;
986
1352
  try {
987
- const dirs = fs2.readdirSync(PROJECTS_DIR);
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 = fs2.readFileSync(filePath, "utf-8").split("\n");
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 = fs2.readdirSync(PROJECTS_DIR);
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 = fs2.statSync(path2.join(PROJECTS_DIR, a));
1064
- const statB = fs2.statSync(path2.join(PROJECTS_DIR, b));
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 = path2.join(PROJECTS_DIR, projDir);
1443
+ const fullPath = path6.join(PROJECTS_DIR, projDir);
1071
1444
  let nSessions = 0;
1072
1445
  try {
1073
- nSessions = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1446
+ nSessions = fs6.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1074
1447
  } catch {
1075
1448
  }
1076
- const stat = fs2.statSync(fullPath);
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 = path2.join(PROJECTS_DIR, projDir);
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 = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
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 = path2.join(fullPath, file);
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 = fs2.readFileSync(filePath, "utf-8");
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 = fs2.readFileSync(filePath, "utf-8").split("\n");
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 searchRoot = PROJECTS_DIR;
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
- searchRoot = path2.join(PROJECTS_DIR, projDir);
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 = fs2.readdirSync(dir, { withFileTypes: true });
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 = path2.join(dir, entry.name);
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 = fs2.readFileSync(fullPath, "utf-8");
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 = path2.relative(PROJECTS_DIR, fullPath);
1184
- const projEnc = relPath.split("/")[0];
1185
- const sessionId = path2.basename(fullPath, ".jsonl");
1186
- console.log(`[${decodePath(projEnc)} \u2192 ${sessionId}]`);
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
- searchDir(searchRoot);
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 = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
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(fs2.readFileSync(path2.join(SESSIONS_DIR, file), "utf-8"));
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 = fs2.readdirSync(PROJECTS_DIR).length;
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 = fs2.readFileSync(f, "utf-8");
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 = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
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
- console.log(`Total messages: ${totalMsgs}`);
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
- try {
1292
- const totalSize = execSync(`du -sh "${path2.join(process.env.CLAUDE_DIR || path2.join(process.env.HOME || "", ".claude"))}" 2>/dev/null`, { encoding: "utf-8" }).trim().split(/\s+/)[0];
1293
- const projSize = execSync(`du -sh "${PROJECTS_DIR}" 2>/dev/null`, { encoding: "utf-8" }).trim().split(/\s+/)[0];
1294
- console.log("Storage:");
1295
- console.log(` Total: ${totalSize}`);
1296
- console.log(` Projects: ${projSize}`);
1297
- } catch {
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 = fs2.readdirSync(dir, { withFileTypes: true });
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 = path2.join(dir, entry.name);
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 = fs2.statSync(fullPath);
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
- fs2.unlinkSync(fullPath);
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 '1:profile:_cc_hub_profiles' '(-m --model)*'{-m,--model}'[Model ID]:model:->profileModel' '(-d --delete-model)*'{-d,--delete-model}'[Remove model ID]:model:->profileModel' '(-t --token)'{-t,--token}'[API key / token]:token:' '(-u --url)'{-u,--url}'[Base URL]:url:' '(-p --provider)'{-p,--provider}'[Provider type]:provider:(anthropic openai)'
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 zsh").action((shell) => {
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 'zsh'.`);
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.0.10",
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"