cc-hub-cli 1.0.10 → 1.0.11

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 +2 -0
  2. package/dist/index.js +273 -69
  3. package/package.json +1 -1
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:** Currently macOS only.
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
package/dist/index.js CHANGED
@@ -18,6 +18,38 @@ var SETTINGS_FILE = process.env.CLAUDE_SETTINGS_FILE || path.join(CLAUDE_DIR, "s
18
18
  var CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
19
19
  var PROJECTS_DIR = path.join(CLAUDE_DIR, "projects");
20
20
  var SESSIONS_DIR = path.join(CLAUDE_DIR, "sessions");
21
+ var DESKTOP_SUPPORT_DIR = path.join(os.homedir(), "Library/Application Support/Claude-3p");
22
+ var DESKTOP_CONFIG_LIBRARY = path.join(DESKTOP_SUPPORT_DIR, "configLibrary");
23
+ var DESKTOP_META_FILE = path.join(DESKTOP_CONFIG_LIBRARY, "_meta.json");
24
+ var DESKTOP_SESSIONS_DIR = path.join(DESKTOP_SUPPORT_DIR, "local-agent-mode-sessions");
25
+ function isDesktopAppInstalled() {
26
+ return fs.existsSync(DESKTOP_SUPPORT_DIR);
27
+ }
28
+ function findDesktopClaudeBinary() {
29
+ const claudeCodeDir = path.join(DESKTOP_SUPPORT_DIR, "claude-code");
30
+ if (!fs.existsSync(claudeCodeDir)) return void 0;
31
+ let versions;
32
+ try {
33
+ versions = fs.readdirSync(claudeCodeDir).filter(
34
+ (d) => fs.existsSync(path.join(claudeCodeDir, d, "claude.app", "Contents", "MacOS", "claude"))
35
+ );
36
+ } catch {
37
+ return void 0;
38
+ }
39
+ if (versions.length === 0) return void 0;
40
+ versions.sort((a, b) => {
41
+ const parse = (v) => v.split(".").map((n) => parseInt(n, 10));
42
+ const av = parse(a);
43
+ const bv = parse(b);
44
+ for (let i = 0; i < Math.max(av.length, bv.length); i++) {
45
+ const an = av[i] || 0;
46
+ const bn = bv[i] || 0;
47
+ if (an !== bn) return bn - an;
48
+ }
49
+ return 0;
50
+ });
51
+ return path.join(claudeCodeDir, versions[0], "claude.app", "Contents", "MacOS", "claude");
52
+ }
21
53
  function ensureFile(filePath, defaultContent) {
22
54
  if (!fs.existsSync(filePath)) {
23
55
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -428,6 +460,9 @@ function providerCommand() {
428
460
  }
429
461
 
430
462
  // src/profiles.ts
463
+ import { randomUUID } from "crypto";
464
+ import fs2 from "fs";
465
+ import path2 from "path";
431
466
  function maskToken(token) {
432
467
  if (!token) return "(unset)";
433
468
  if (token.length <= 12) return token;
@@ -463,21 +498,114 @@ function isAnthropicModel(model) {
463
498
  if (lower.startsWith("claude-")) return true;
464
499
  return false;
465
500
  }
501
+ function toDesktopProfile(p) {
502
+ const models = p.models || (p.model ? [p.model] : []);
503
+ const isAnthropic = p.provider === "anthropic" || !p.provider && !p.url;
504
+ if (isAnthropic && !p.url) {
505
+ return {
506
+ inferenceProvider: "1p",
507
+ inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
508
+ };
509
+ }
510
+ return {
511
+ inferenceProvider: "gateway",
512
+ inferenceGatewayBaseUrl: p.url || void 0,
513
+ inferenceGatewayApiKey: p.token || void 0,
514
+ inferenceGatewayAuthScheme: "bearer",
515
+ inferenceModels: models.map((m) => ({ name: m, supports1m: true }))
516
+ };
517
+ }
518
+ function readDesktopMeta() {
519
+ if (!fs2.existsSync(DESKTOP_META_FILE)) return {};
520
+ try {
521
+ return readJson(DESKTOP_META_FILE);
522
+ } catch {
523
+ return {};
524
+ }
525
+ }
526
+ function writeDesktopMeta(meta) {
527
+ writeJson(DESKTOP_META_FILE, meta);
528
+ }
529
+ function writeDesktopProfile(id, data) {
530
+ const filePath = path2.join(DESKTOP_CONFIG_LIBRARY, `${id}.json`);
531
+ writeJson(filePath, data);
532
+ }
533
+ function removeDesktopProfile(id) {
534
+ const filePath = path2.join(DESKTOP_CONFIG_LIBRARY, `${id}.json`);
535
+ if (fs2.existsSync(filePath)) {
536
+ fs2.unlinkSync(filePath);
537
+ }
538
+ }
539
+ function syncProfileToDesktop(name, p) {
540
+ if (!isDesktopAppInstalled()) return;
541
+ if (!fs2.existsSync(DESKTOP_CONFIG_LIBRARY)) {
542
+ fs2.mkdirSync(DESKTOP_CONFIG_LIBRARY, { recursive: true });
543
+ }
544
+ const meta = readDesktopMeta();
545
+ const entries = meta.entries || [];
546
+ let id = p.desktopId;
547
+ if (!id) {
548
+ const existingByName = entries.find((e) => e.name === name);
549
+ if (existingByName) {
550
+ id = existingByName.id;
551
+ } else {
552
+ id = randomUUID();
553
+ }
554
+ p.desktopId = id;
555
+ }
556
+ const existingIndex = entries.findIndex((e) => e.id === id);
557
+ if (existingIndex !== -1) {
558
+ entries[existingIndex].name = name;
559
+ } else {
560
+ entries.push({ id, name });
561
+ }
562
+ meta.entries = entries;
563
+ writeDesktopMeta(meta);
564
+ writeDesktopProfile(id, toDesktopProfile(p));
565
+ }
566
+ function removeProfileFromDesktop(name, p) {
567
+ if (!isDesktopAppInstalled() || !p.desktopId) return;
568
+ const meta = readDesktopMeta();
569
+ if (meta.entries) {
570
+ meta.entries = meta.entries.filter((e) => e.id !== p.desktopId);
571
+ }
572
+ if (meta.appliedId === p.desktopId) {
573
+ delete meta.appliedId;
574
+ }
575
+ writeDesktopMeta(meta);
576
+ removeDesktopProfile(p.desktopId);
577
+ }
578
+ function setDesktopActiveProfile(p) {
579
+ if (!isDesktopAppInstalled() || !p.desktopId) return;
580
+ const meta = readDesktopMeta();
581
+ meta.appliedId = p.desktopId;
582
+ const entries = meta.entries || [];
583
+ if (!entries.some((e) => e.id === p.desktopId)) {
584
+ entries.push({ id: p.desktopId, name: "unknown" });
585
+ meta.entries = entries;
586
+ }
587
+ writeDesktopMeta(meta);
588
+ }
589
+ function resolveClaudeBinary() {
590
+ try {
591
+ const result = spawnSync("which", ["claude"], { encoding: "utf-8" });
592
+ if (result.status === 0 && result.stdout.trim()) {
593
+ return "claude";
594
+ }
595
+ } catch {
596
+ }
597
+ const desktopBinary = findDesktopClaudeBinary();
598
+ if (desktopBinary) return desktopBinary;
599
+ console.error("Error: Could not find Claude Code CLI.");
600
+ console.error("Install it globally or install the Claude Code desktop app.");
601
+ process.exit(1);
602
+ }
466
603
  function updateSettingsForProfile(p) {
467
604
  ensureSettingsFile();
468
605
  const settings = readJson(SETTINGS_FILE);
469
606
  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
- }
607
+ delete settings.model;
608
+ delete settings.availableModels;
481
609
  const envVarsToClean = [
482
610
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
483
611
  "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
@@ -499,11 +627,15 @@ function updateSettingsForProfile(p) {
499
627
  }
500
628
  function profileCommand() {
501
629
  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) => {
630
+ 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 (max 3)", 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) => {
631
+ const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
632
+ if (models && models.length > 3) {
633
+ console.error("Error: A profile can have at most 3 models.");
634
+ process.exit(1);
635
+ }
503
636
  ensureProfilesFile();
504
637
  const data = readJson(PROFILES_FILE);
505
638
  const profile2 = data.profiles[name] || {};
506
- const models = opts.model && opts.model.length > 0 ? opts.model : void 0;
507
639
  if (models) {
508
640
  profile2.models = models;
509
641
  profile2.model = models[0];
@@ -512,10 +644,11 @@ function profileCommand() {
512
644
  if (opts.url) profile2.url = opts.url;
513
645
  if (opts.provider) profile2.provider = opts.provider;
514
646
  data.profiles[name] = profile2;
647
+ syncProfileToDesktop(name, profile2);
515
648
  writeJson(PROFILES_FILE, data);
516
649
  console.log(`Profile '${name}' saved.`);
517
650
  });
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) => {
651
+ 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 (max 3)", 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) => {
519
652
  ensureProfilesFile();
520
653
  const data = readJson(PROFILES_FILE);
521
654
  if (!data.profiles[name]) {
@@ -564,9 +697,15 @@ function profileCommand() {
564
697
  p.model = providedModels[0];
565
698
  }
566
699
  }
700
+ const finalModels = p.models || (p.model ? [p.model] : []);
701
+ if (finalModels.length > 3) {
702
+ console.error("Error: A profile can have at most 3 models.");
703
+ process.exit(1);
704
+ }
567
705
  if (opts.token) p.token = opts.token;
568
706
  if (opts.url) p.url = opts.url;
569
707
  if (opts.provider) p.provider = opts.provider;
708
+ syncProfileToDesktop(name, p);
570
709
  writeJson(PROFILES_FILE, data);
571
710
  console.log(`Profile '${name}' updated.`);
572
711
  });
@@ -586,9 +725,11 @@ function profileCommand() {
586
725
  for (const name of names) {
587
726
  const p = profiles[name];
588
727
  const marker = name === def ? "* " : " ";
728
+ const desktopMarker = p.desktopId ? " [desktop]" : "";
729
+ const displayName = (name + desktopMarker).padEnd(20);
589
730
  console.log(fmt(
590
731
  marker,
591
- name,
732
+ displayName,
592
733
  formatModels(p),
593
734
  maskToken(p.token || ""),
594
735
  p.provider || "anthropic",
@@ -605,7 +746,8 @@ function profileCommand() {
605
746
  process.exit(1);
606
747
  }
607
748
  if (opts.json) {
608
- console.log(JSON.stringify({ name, ...p }, null, 2));
749
+ const { desktopId, ...rest } = p;
750
+ console.log(JSON.stringify({ name, ...rest }, null, 2));
609
751
  } else {
610
752
  console.log(`Name: ${name}`);
611
753
  console.log(`Model: ${p.model || "(unset)"}`);
@@ -637,6 +779,7 @@ function profileCommand() {
637
779
  console.error(`Profile '${name}' not found.`);
638
780
  process.exit(1);
639
781
  }
782
+ removeProfileFromDesktop(name, data.profiles[name]);
640
783
  delete data.profiles[name];
641
784
  writeJson(PROFILES_FILE, data);
642
785
  console.log(`Profile '${name}' removed.`);
@@ -668,9 +811,32 @@ function profileCommand() {
668
811
  process.exit(1);
669
812
  }
670
813
  data.default = name;
814
+ setDesktopActiveProfile(data.profiles[name]);
671
815
  writeJson(PROFILES_FILE, data);
672
816
  console.log(`Default profile set to '${name}'.`);
673
817
  });
818
+ profile.command("sync").description("Synchronize all CLI profiles to the Claude desktop app").action(() => {
819
+ if (!isDesktopAppInstalled()) {
820
+ console.error("Claude desktop app is not installed.");
821
+ process.exit(1);
822
+ }
823
+ ensureProfilesFile();
824
+ const data = readJson(PROFILES_FILE);
825
+ const names = Object.keys(data.profiles);
826
+ if (names.length === 0) {
827
+ console.log("No profiles to sync.");
828
+ return;
829
+ }
830
+ if (!fs2.existsSync(DESKTOP_CONFIG_LIBRARY)) {
831
+ fs2.mkdirSync(DESKTOP_CONFIG_LIBRARY, { recursive: true });
832
+ }
833
+ for (const name of names) {
834
+ const p = data.profiles[name];
835
+ syncProfileToDesktop(name, p);
836
+ }
837
+ writeJson(PROFILES_FILE, data);
838
+ console.log(`Synced ${names.length} profile(s) to the desktop app.`);
839
+ });
674
840
  return profile;
675
841
  }
676
842
  function collect(value, previous) {
@@ -680,7 +846,8 @@ function execClaude(profileName, p, extraArgs) {
680
846
  updateSettingsForProfile(p);
681
847
  const models = p.models || (p.model ? [p.model] : []);
682
848
  const firstModel = models[0];
683
- const cmd = ["claude"];
849
+ const binary = resolveClaudeBinary();
850
+ const cmd = [binary];
684
851
  if (firstModel) cmd.push("--model", firstModel);
685
852
  cmd.push(...extraArgs);
686
853
  const env = {
@@ -745,6 +912,7 @@ function useCommand() {
745
912
  process.exit(1);
746
913
  }
747
914
  data.default = name;
915
+ setDesktopActiveProfile(data.profiles[name]);
748
916
  writeJson(PROFILES_FILE, data);
749
917
  console.log(`Default profile set to '${name}'.`);
750
918
  });
@@ -966,8 +1134,8 @@ function hooksCommand() {
966
1134
 
967
1135
  // src/sessions.ts
968
1136
  import { Command as Command4 } from "commander";
969
- import fs2 from "fs";
970
- import path2 from "path";
1137
+ import fs3 from "fs";
1138
+ import path3 from "path";
971
1139
  import { execSync } from "child_process";
972
1140
  function encodePath(p) {
973
1141
  return p.replace(/\./g, "DOTMARK").replace(/\//g, "-").replace(/DOTMARK/g, "-");
@@ -982,9 +1150,9 @@ function formatTimestamp(ms) {
982
1150
  }
983
1151
  function findProjectDir(query) {
984
1152
  const encoded = encodePath(query);
985
- if (fs2.existsSync(path2.join(PROJECTS_DIR, encoded))) return encoded;
1153
+ if (fs3.existsSync(path3.join(PROJECTS_DIR, encoded))) return encoded;
986
1154
  try {
987
- const dirs = fs2.readdirSync(PROJECTS_DIR);
1155
+ const dirs = fs3.readdirSync(PROJECTS_DIR);
988
1156
  const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
989
1157
  return match || null;
990
1158
  } catch {
@@ -996,7 +1164,7 @@ function parseSessionMeta(filePath) {
996
1164
  let slug = "";
997
1165
  let customTitle = "";
998
1166
  try {
999
- const lines = fs2.readFileSync(filePath, "utf-8").split("\n");
1167
+ const lines = fs3.readFileSync(filePath, "utf-8").split("\n");
1000
1168
  for (const line of lines) {
1001
1169
  if (!line.trim()) continue;
1002
1170
  try {
@@ -1054,26 +1222,26 @@ function sessionCommand() {
1054
1222
  const limit = parseInt(opts.limit, 10);
1055
1223
  let dirs;
1056
1224
  try {
1057
- dirs = fs2.readdirSync(PROJECTS_DIR);
1225
+ dirs = fs3.readdirSync(PROJECTS_DIR);
1058
1226
  } catch {
1059
1227
  console.log("No projects directory found.");
1060
1228
  return;
1061
1229
  }
1062
1230
  dirs.sort((a, b) => {
1063
- const statA = fs2.statSync(path2.join(PROJECTS_DIR, a));
1064
- const statB = fs2.statSync(path2.join(PROJECTS_DIR, b));
1231
+ const statA = fs3.statSync(path3.join(PROJECTS_DIR, a));
1232
+ const statB = fs3.statSync(path3.join(PROJECTS_DIR, b));
1065
1233
  return statB.mtimeMs - statA.mtimeMs;
1066
1234
  });
1067
1235
  let count = 0;
1068
1236
  for (const projDir of dirs) {
1069
1237
  if (count >= limit) break;
1070
- const fullPath = path2.join(PROJECTS_DIR, projDir);
1238
+ const fullPath = path3.join(PROJECTS_DIR, projDir);
1071
1239
  let nSessions = 0;
1072
1240
  try {
1073
- nSessions = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1241
+ nSessions = fs3.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1074
1242
  } catch {
1075
1243
  }
1076
- const stat = fs2.statSync(fullPath);
1244
+ const stat = fs3.statSync(fullPath);
1077
1245
  const decoded = decodePath(projDir);
1078
1246
  if (opts.json) {
1079
1247
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
@@ -1091,7 +1259,7 @@ function sessionCommand() {
1091
1259
  console.error(`No project matched: ${project}`);
1092
1260
  process.exit(1);
1093
1261
  }
1094
- const fullPath = path2.join(PROJECTS_DIR, projDir);
1262
+ const fullPath = path3.join(PROJECTS_DIR, projDir);
1095
1263
  console.log(`Project: ${decodePath(projDir)}`);
1096
1264
  console.log(`Dir: ${fullPath}`);
1097
1265
  console.log("");
@@ -1100,16 +1268,16 @@ function sessionCommand() {
1100
1268
  console.log(fmt("----------", "----", "-------", "--------"));
1101
1269
  let files;
1102
1270
  try {
1103
- files = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1271
+ files = fs3.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1104
1272
  } catch {
1105
1273
  return;
1106
1274
  }
1107
1275
  for (const file of files) {
1108
- const filePath = path2.join(fullPath, file);
1276
+ const filePath = path3.join(fullPath, file);
1109
1277
  const sessionId = file.replace(/\.jsonl$/, "");
1110
1278
  let msgCount = 0;
1111
1279
  try {
1112
- const content = fs2.readFileSync(filePath, "utf-8");
1280
+ const content = fs3.readFileSync(filePath, "utf-8");
1113
1281
  msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
1114
1282
  } catch {
1115
1283
  }
@@ -1117,7 +1285,7 @@ function sessionCommand() {
1117
1285
  console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
1118
1286
  if (opts.verbose) {
1119
1287
  try {
1120
- const lines = fs2.readFileSync(filePath, "utf-8").split("\n");
1288
+ const lines = fs3.readFileSync(filePath, "utf-8").split("\n");
1121
1289
  for (const line of lines) {
1122
1290
  if (!line.trim()) continue;
1123
1291
  try {
@@ -1145,33 +1313,36 @@ function sessionCommand() {
1145
1313
  }
1146
1314
  });
1147
1315
  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;
1316
+ let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
1317
+ if (isDesktopAppInstalled()) {
1318
+ searchRoots.push({ root: DESKTOP_SESSIONS_DIR, label: "[desktop] " });
1319
+ }
1149
1320
  if (opts.project) {
1150
1321
  const projDir = findProjectDir(opts.project);
1151
1322
  if (!projDir) {
1152
1323
  console.error(`No project matched: ${opts.project}`);
1153
1324
  process.exit(1);
1154
1325
  }
1155
- searchRoot = path2.join(PROJECTS_DIR, projDir);
1326
+ searchRoots = [{ root: path3.join(PROJECTS_DIR, projDir), label: "" }];
1156
1327
  }
1157
1328
  const limit = parseInt(opts.limit, 10);
1158
1329
  let count = 0;
1159
- function searchDir(dir) {
1330
+ function searchDir(dir, label, baseDir) {
1160
1331
  if (count >= limit) return;
1161
1332
  let entries;
1162
1333
  try {
1163
- entries = fs2.readdirSync(dir, { withFileTypes: true });
1334
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1164
1335
  } catch {
1165
1336
  return;
1166
1337
  }
1167
1338
  for (const entry of entries) {
1168
1339
  if (count >= limit) break;
1169
- const fullPath = path2.join(dir, entry.name);
1340
+ const fullPath = path3.join(dir, entry.name);
1170
1341
  if (entry.isDirectory()) {
1171
- searchDir(fullPath);
1342
+ searchDir(fullPath, label, baseDir);
1172
1343
  } else if (entry.name.endsWith(".jsonl")) {
1173
1344
  try {
1174
- const content = fs2.readFileSync(fullPath, "utf-8");
1345
+ const content = fs3.readFileSync(fullPath, "utf-8");
1175
1346
  const lines = content.split("\n");
1176
1347
  let found = false;
1177
1348
  for (let lineno = 0; lineno < lines.length; lineno++) {
@@ -1180,10 +1351,11 @@ function sessionCommand() {
1180
1351
  const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
1181
1352
  if (match) {
1182
1353
  if (!found) {
1183
- const relPath = path2.relative(PROJECTS_DIR, fullPath);
1354
+ const relPath = path3.relative(baseDir, fullPath);
1184
1355
  const projEnc = relPath.split("/")[0];
1185
- const sessionId = path2.basename(fullPath, ".jsonl");
1186
- console.log(`[${decodePath(projEnc)} \u2192 ${sessionId}]`);
1356
+ const sessionId = path3.basename(fullPath, ".jsonl");
1357
+ const projName = label ? projEnc : decodePath(projEnc);
1358
+ console.log(`${label}[${projName} \u2192 ${sessionId}]`);
1187
1359
  found = true;
1188
1360
  count++;
1189
1361
  }
@@ -1211,12 +1383,14 @@ function sessionCommand() {
1211
1383
  }
1212
1384
  }
1213
1385
  }
1214
- searchDir(searchRoot);
1386
+ for (const { root, label } of searchRoots) {
1387
+ searchDir(root, label, root);
1388
+ }
1215
1389
  });
1216
1390
  session.command("ps").description("Show active Claude Code processes").action(() => {
1217
1391
  let files;
1218
1392
  try {
1219
- files = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1393
+ files = fs3.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1220
1394
  } catch {
1221
1395
  console.log("(no session files found)");
1222
1396
  return;
@@ -1230,7 +1404,7 @@ function sessionCommand() {
1230
1404
  console.log(fmt("---", "----------", "-------", "---", ""));
1231
1405
  for (const file of files) {
1232
1406
  try {
1233
- const data = JSON.parse(fs2.readFileSync(path2.join(SESSIONS_DIR, file), "utf-8"));
1407
+ const data = JSON.parse(fs3.readFileSync(path3.join(SESSIONS_DIR, file), "utf-8"));
1234
1408
  const pid = String(data.pid || "?");
1235
1409
  const sessionId = data.sessionId || "?";
1236
1410
  const cwd = data.cwd || "?";
@@ -1251,49 +1425,75 @@ function sessionCommand() {
1251
1425
  let nSessions = 0;
1252
1426
  let totalMsgs = 0;
1253
1427
  let nActive = 0;
1428
+ let nDesktopSessions = 0;
1429
+ let nDesktopMsgs = 0;
1430
+ const walk = (dir) => {
1431
+ const results = [];
1432
+ try {
1433
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
1434
+ const fullPath = path3.join(dir, entry.name);
1435
+ if (entry.isDirectory()) results.push(...walk(fullPath));
1436
+ else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
1437
+ }
1438
+ } catch {
1439
+ }
1440
+ return results;
1441
+ };
1254
1442
  try {
1255
- nProjects = fs2.readdirSync(PROJECTS_DIR).length;
1443
+ nProjects = fs3.readdirSync(PROJECTS_DIR).length;
1256
1444
  } catch {
1257
1445
  }
1258
1446
  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
1447
  const sessionFiles = walk(PROJECTS_DIR);
1272
1448
  nSessions = sessionFiles.length;
1273
1449
  for (const f of sessionFiles) {
1274
1450
  try {
1275
- const content = fs2.readFileSync(f, "utf-8");
1451
+ const content = fs3.readFileSync(f, "utf-8");
1276
1452
  totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1277
1453
  } catch {
1278
1454
  }
1279
1455
  }
1280
1456
  } catch {
1281
1457
  }
1458
+ if (isDesktopAppInstalled()) {
1459
+ try {
1460
+ const desktopFiles = walk(DESKTOP_SESSIONS_DIR);
1461
+ nDesktopSessions = desktopFiles.length;
1462
+ for (const f of desktopFiles) {
1463
+ try {
1464
+ const content = fs3.readFileSync(f, "utf-8");
1465
+ nDesktopMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1466
+ } catch {
1467
+ }
1468
+ }
1469
+ } catch {
1470
+ }
1471
+ }
1282
1472
  try {
1283
- nActive = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1473
+ nActive = fs3.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1284
1474
  } catch {
1285
1475
  }
1286
1476
  console.log(`Projects: ${nProjects}`);
1287
- console.log(`Sessions: ${nSessions}`);
1288
- console.log(`Total messages: ${totalMsgs}`);
1477
+ console.log(`Sessions: ${nSessions} (CLI)`);
1478
+ if (isDesktopAppInstalled()) {
1479
+ console.log(` ${nDesktopSessions} (desktop)`);
1480
+ }
1481
+ console.log(`Total messages: ${totalMsgs} (CLI)`);
1482
+ if (isDesktopAppInstalled()) {
1483
+ console.log(` ${nDesktopMsgs} (desktop)`);
1484
+ }
1289
1485
  console.log(`Active procs: ${nActive} (in ${SESSIONS_DIR})`);
1290
1486
  console.log("");
1291
1487
  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];
1488
+ const totalSize = execSync(`du -sh "${path3.join(process.env.CLAUDE_DIR || path3.join(process.env.HOME || "", ".claude"))}" 2>/dev/null`, { encoding: "utf-8" }).trim().split(/\s+/)[0];
1293
1489
  const projSize = execSync(`du -sh "${PROJECTS_DIR}" 2>/dev/null`, { encoding: "utf-8" }).trim().split(/\s+/)[0];
1490
+ const desktopSize = isDesktopAppInstalled() ? execSync(`du -sh "${DESKTOP_SESSIONS_DIR}" 2>/dev/null`, { encoding: "utf-8" }).trim().split(/\s+/)[0] : null;
1294
1491
  console.log("Storage:");
1295
1492
  console.log(` Total: ${totalSize}`);
1296
1493
  console.log(` Projects: ${projSize}`);
1494
+ if (desktopSize) {
1495
+ console.log(` Desktop: ${desktopSize}`);
1496
+ }
1297
1497
  } catch {
1298
1498
  }
1299
1499
  });
@@ -1305,23 +1505,23 @@ function sessionCommand() {
1305
1505
  const walk = (dir) => {
1306
1506
  let entries;
1307
1507
  try {
1308
- entries = fs2.readdirSync(dir, { withFileTypes: true });
1508
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1309
1509
  } catch {
1310
1510
  return;
1311
1511
  }
1312
1512
  for (const entry of entries) {
1313
- const fullPath = path2.join(dir, entry.name);
1513
+ const fullPath = path3.join(dir, entry.name);
1314
1514
  if (entry.isDirectory()) {
1315
1515
  walk(fullPath);
1316
1516
  } else if (entry.name.endsWith(".jsonl")) {
1317
1517
  try {
1318
- const stat = fs2.statSync(fullPath);
1518
+ const stat = fs3.statSync(fullPath);
1319
1519
  if (stat.mtimeMs < cutoffMs) {
1320
1520
  const size = stat.size;
1321
1521
  if (opts.dryRun) {
1322
1522
  console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
1323
1523
  } else {
1324
- fs2.unlinkSync(fullPath);
1524
+ fs3.unlinkSync(fullPath);
1325
1525
  console.log(`Deleted: ${fullPath}`);
1326
1526
  }
1327
1527
  deleted++;
@@ -1333,6 +1533,9 @@ function sessionCommand() {
1333
1533
  }
1334
1534
  };
1335
1535
  walk(PROJECTS_DIR);
1536
+ if (isDesktopAppInstalled()) {
1537
+ walk(DESKTOP_SESSIONS_DIR);
1538
+ }
1336
1539
  console.log("");
1337
1540
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1338
1541
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
@@ -1372,6 +1575,7 @@ _cc-hub() {
1372
1575
  'remove:Remove a profile'
1373
1576
  'rename:Rename a profile'
1374
1577
  'default:Set the default profile'
1578
+ 'sync:Synchronize all CLI profiles to the desktop app'
1375
1579
  )
1376
1580
 
1377
1581
  local -a hooks_subcmds
@@ -1515,7 +1719,7 @@ _cc-hub() {
1515
1719
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
1516
1720
  commands="profile use run hook session provider complete help"
1517
1721
 
1518
- local profile_subcmds="add update list view remove rename default"
1722
+ local profile_subcmds="add update list view remove rename default sync"
1519
1723
  local provider_subcmds="list"
1520
1724
  local provider_types="anthropic openai"
1521
1725
  local hooks_subcmds="list add remove enable disable"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {