cc-hub-cli 1.0.9 → 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 +301 -100
  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,40 +697,18 @@ 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
  });
573
- profile.command("remove-model").description("Remove specific models from a profile").argument("<name>", "Profile name").option("-m, --model <model>", "Model ID to remove - can be used multiple times", collect, []).action((name, opts) => {
574
- ensureProfilesFile();
575
- const data = readJson(PROFILES_FILE);
576
- if (!data.profiles[name]) {
577
- console.error(`Profile '${name}' not found.`);
578
- process.exit(1);
579
- }
580
- const p = data.profiles[name];
581
- const toRemove = new Set(opts.model);
582
- if (toRemove.size === 0) {
583
- console.error("No models specified to remove. Use -m <model> to specify models.");
584
- process.exit(1);
585
- }
586
- const currentModels = p.models || (p.model ? [p.model] : []);
587
- const newModels = currentModels.filter((m) => !toRemove.has(m));
588
- if (newModels.length === 0) {
589
- delete p.models;
590
- delete p.model;
591
- console.log(`Removed all models from profile '${name}'.`);
592
- } else {
593
- const removedCount = currentModels.length - newModels.length;
594
- p.models = newModels;
595
- p.model = newModels[0];
596
- console.log(`Removed ${removedCount} model(s) from profile '${name}'.`);
597
- console.log(`Remaining models: ${newModels.join(", ")}`);
598
- }
599
- writeJson(PROFILES_FILE, data);
600
- });
601
712
  profile.command("list").description("List all profiles").action(() => {
602
713
  ensureProfilesFile();
603
714
  const data = readJson(PROFILES_FILE);
@@ -614,9 +725,11 @@ function profileCommand() {
614
725
  for (const name of names) {
615
726
  const p = profiles[name];
616
727
  const marker = name === def ? "* " : " ";
728
+ const desktopMarker = p.desktopId ? " [desktop]" : "";
729
+ const displayName = (name + desktopMarker).padEnd(20);
617
730
  console.log(fmt(
618
731
  marker,
619
- name,
732
+ displayName,
620
733
  formatModels(p),
621
734
  maskToken(p.token || ""),
622
735
  p.provider || "anthropic",
@@ -633,7 +746,8 @@ function profileCommand() {
633
746
  process.exit(1);
634
747
  }
635
748
  if (opts.json) {
636
- console.log(JSON.stringify({ name, ...p }, null, 2));
749
+ const { desktopId, ...rest } = p;
750
+ console.log(JSON.stringify({ name, ...rest }, null, 2));
637
751
  } else {
638
752
  console.log(`Name: ${name}`);
639
753
  console.log(`Model: ${p.model || "(unset)"}`);
@@ -665,10 +779,30 @@ function profileCommand() {
665
779
  console.error(`Profile '${name}' not found.`);
666
780
  process.exit(1);
667
781
  }
782
+ removeProfileFromDesktop(name, data.profiles[name]);
668
783
  delete data.profiles[name];
669
784
  writeJson(PROFILES_FILE, data);
670
785
  console.log(`Profile '${name}' removed.`);
671
786
  });
787
+ profile.command("rename").description("Rename a profile").argument("<oldName>", "Current profile name").argument("<newName>", "New profile name").action((oldName, newName) => {
788
+ ensureProfilesFile();
789
+ const data = readJson(PROFILES_FILE);
790
+ if (!data.profiles[oldName]) {
791
+ console.error(`Profile '${oldName}' not found.`);
792
+ process.exit(1);
793
+ }
794
+ if (data.profiles[newName]) {
795
+ console.error(`Profile '${newName}' already exists. Choose a different name.`);
796
+ process.exit(1);
797
+ }
798
+ data.profiles[newName] = data.profiles[oldName];
799
+ delete data.profiles[oldName];
800
+ if (data.default === oldName) {
801
+ data.default = newName;
802
+ }
803
+ writeJson(PROFILES_FILE, data);
804
+ console.log(`Profile '${oldName}' renamed to '${newName}'.`);
805
+ });
672
806
  profile.command("default").description("Set the default profile").argument("<name>", "Profile name to set as default").action((name) => {
673
807
  ensureProfilesFile();
674
808
  const data = readJson(PROFILES_FILE);
@@ -677,9 +811,32 @@ function profileCommand() {
677
811
  process.exit(1);
678
812
  }
679
813
  data.default = name;
814
+ setDesktopActiveProfile(data.profiles[name]);
680
815
  writeJson(PROFILES_FILE, data);
681
816
  console.log(`Default profile set to '${name}'.`);
682
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
+ });
683
840
  return profile;
684
841
  }
685
842
  function collect(value, previous) {
@@ -689,7 +846,8 @@ function execClaude(profileName, p, extraArgs) {
689
846
  updateSettingsForProfile(p);
690
847
  const models = p.models || (p.model ? [p.model] : []);
691
848
  const firstModel = models[0];
692
- const cmd = ["claude"];
849
+ const binary = resolveClaudeBinary();
850
+ const cmd = [binary];
693
851
  if (firstModel) cmd.push("--model", firstModel);
694
852
  cmd.push(...extraArgs);
695
853
  const env = {
@@ -754,6 +912,7 @@ function useCommand() {
754
912
  process.exit(1);
755
913
  }
756
914
  data.default = name;
915
+ setDesktopActiveProfile(data.profiles[name]);
757
916
  writeJson(PROFILES_FILE, data);
758
917
  console.log(`Default profile set to '${name}'.`);
759
918
  });
@@ -975,8 +1134,8 @@ function hooksCommand() {
975
1134
 
976
1135
  // src/sessions.ts
977
1136
  import { Command as Command4 } from "commander";
978
- import fs2 from "fs";
979
- import path2 from "path";
1137
+ import fs3 from "fs";
1138
+ import path3 from "path";
980
1139
  import { execSync } from "child_process";
981
1140
  function encodePath(p) {
982
1141
  return p.replace(/\./g, "DOTMARK").replace(/\//g, "-").replace(/DOTMARK/g, "-");
@@ -991,9 +1150,9 @@ function formatTimestamp(ms) {
991
1150
  }
992
1151
  function findProjectDir(query) {
993
1152
  const encoded = encodePath(query);
994
- if (fs2.existsSync(path2.join(PROJECTS_DIR, encoded))) return encoded;
1153
+ if (fs3.existsSync(path3.join(PROJECTS_DIR, encoded))) return encoded;
995
1154
  try {
996
- const dirs = fs2.readdirSync(PROJECTS_DIR);
1155
+ const dirs = fs3.readdirSync(PROJECTS_DIR);
997
1156
  const match = dirs.find((d) => d.toLowerCase().includes(query.toLowerCase()));
998
1157
  return match || null;
999
1158
  } catch {
@@ -1005,7 +1164,7 @@ function parseSessionMeta(filePath) {
1005
1164
  let slug = "";
1006
1165
  let customTitle = "";
1007
1166
  try {
1008
- const lines = fs2.readFileSync(filePath, "utf-8").split("\n");
1167
+ const lines = fs3.readFileSync(filePath, "utf-8").split("\n");
1009
1168
  for (const line of lines) {
1010
1169
  if (!line.trim()) continue;
1011
1170
  try {
@@ -1063,26 +1222,26 @@ function sessionCommand() {
1063
1222
  const limit = parseInt(opts.limit, 10);
1064
1223
  let dirs;
1065
1224
  try {
1066
- dirs = fs2.readdirSync(PROJECTS_DIR);
1225
+ dirs = fs3.readdirSync(PROJECTS_DIR);
1067
1226
  } catch {
1068
1227
  console.log("No projects directory found.");
1069
1228
  return;
1070
1229
  }
1071
1230
  dirs.sort((a, b) => {
1072
- const statA = fs2.statSync(path2.join(PROJECTS_DIR, a));
1073
- 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));
1074
1233
  return statB.mtimeMs - statA.mtimeMs;
1075
1234
  });
1076
1235
  let count = 0;
1077
1236
  for (const projDir of dirs) {
1078
1237
  if (count >= limit) break;
1079
- const fullPath = path2.join(PROJECTS_DIR, projDir);
1238
+ const fullPath = path3.join(PROJECTS_DIR, projDir);
1080
1239
  let nSessions = 0;
1081
1240
  try {
1082
- nSessions = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1241
+ nSessions = fs3.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl")).length;
1083
1242
  } catch {
1084
1243
  }
1085
- const stat = fs2.statSync(fullPath);
1244
+ const stat = fs3.statSync(fullPath);
1086
1245
  const decoded = decodePath(projDir);
1087
1246
  if (opts.json) {
1088
1247
  console.log(JSON.stringify({ project: decoded, sessions: nSessions, modified: Math.floor(stat.mtimeMs) }));
@@ -1100,7 +1259,7 @@ function sessionCommand() {
1100
1259
  console.error(`No project matched: ${project}`);
1101
1260
  process.exit(1);
1102
1261
  }
1103
- const fullPath = path2.join(PROJECTS_DIR, projDir);
1262
+ const fullPath = path3.join(PROJECTS_DIR, projDir);
1104
1263
  console.log(`Project: ${decodePath(projDir)}`);
1105
1264
  console.log(`Dir: ${fullPath}`);
1106
1265
  console.log("");
@@ -1109,16 +1268,16 @@ function sessionCommand() {
1109
1268
  console.log(fmt("----------", "----", "-------", "--------"));
1110
1269
  let files;
1111
1270
  try {
1112
- files = fs2.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1271
+ files = fs3.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
1113
1272
  } catch {
1114
1273
  return;
1115
1274
  }
1116
1275
  for (const file of files) {
1117
- const filePath = path2.join(fullPath, file);
1276
+ const filePath = path3.join(fullPath, file);
1118
1277
  const sessionId = file.replace(/\.jsonl$/, "");
1119
1278
  let msgCount = 0;
1120
1279
  try {
1121
- const content = fs2.readFileSync(filePath, "utf-8");
1280
+ const content = fs3.readFileSync(filePath, "utf-8");
1122
1281
  msgCount = content ? content.split("\n").filter((l) => l.trim()).length : 0;
1123
1282
  } catch {
1124
1283
  }
@@ -1126,7 +1285,7 @@ function sessionCommand() {
1126
1285
  console.log(fmt(sessionId, slug || "-", started, String(msgCount)));
1127
1286
  if (opts.verbose) {
1128
1287
  try {
1129
- const lines = fs2.readFileSync(filePath, "utf-8").split("\n");
1288
+ const lines = fs3.readFileSync(filePath, "utf-8").split("\n");
1130
1289
  for (const line of lines) {
1131
1290
  if (!line.trim()) continue;
1132
1291
  try {
@@ -1154,33 +1313,36 @@ function sessionCommand() {
1154
1313
  }
1155
1314
  });
1156
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) => {
1157
- let searchRoot = PROJECTS_DIR;
1316
+ let searchRoots = [{ root: PROJECTS_DIR, label: "" }];
1317
+ if (isDesktopAppInstalled()) {
1318
+ searchRoots.push({ root: DESKTOP_SESSIONS_DIR, label: "[desktop] " });
1319
+ }
1158
1320
  if (opts.project) {
1159
1321
  const projDir = findProjectDir(opts.project);
1160
1322
  if (!projDir) {
1161
1323
  console.error(`No project matched: ${opts.project}`);
1162
1324
  process.exit(1);
1163
1325
  }
1164
- searchRoot = path2.join(PROJECTS_DIR, projDir);
1326
+ searchRoots = [{ root: path3.join(PROJECTS_DIR, projDir), label: "" }];
1165
1327
  }
1166
1328
  const limit = parseInt(opts.limit, 10);
1167
1329
  let count = 0;
1168
- function searchDir(dir) {
1330
+ function searchDir(dir, label, baseDir) {
1169
1331
  if (count >= limit) return;
1170
1332
  let entries;
1171
1333
  try {
1172
- entries = fs2.readdirSync(dir, { withFileTypes: true });
1334
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1173
1335
  } catch {
1174
1336
  return;
1175
1337
  }
1176
1338
  for (const entry of entries) {
1177
1339
  if (count >= limit) break;
1178
- const fullPath = path2.join(dir, entry.name);
1340
+ const fullPath = path3.join(dir, entry.name);
1179
1341
  if (entry.isDirectory()) {
1180
- searchDir(fullPath);
1342
+ searchDir(fullPath, label, baseDir);
1181
1343
  } else if (entry.name.endsWith(".jsonl")) {
1182
1344
  try {
1183
- const content = fs2.readFileSync(fullPath, "utf-8");
1345
+ const content = fs3.readFileSync(fullPath, "utf-8");
1184
1346
  const lines = content.split("\n");
1185
1347
  let found = false;
1186
1348
  for (let lineno = 0; lineno < lines.length; lineno++) {
@@ -1189,10 +1351,11 @@ function sessionCommand() {
1189
1351
  const match = opts.ignoreCase ? line.toLowerCase().includes(query.toLowerCase()) : line.includes(query);
1190
1352
  if (match) {
1191
1353
  if (!found) {
1192
- const relPath = path2.relative(PROJECTS_DIR, fullPath);
1354
+ const relPath = path3.relative(baseDir, fullPath);
1193
1355
  const projEnc = relPath.split("/")[0];
1194
- const sessionId = path2.basename(fullPath, ".jsonl");
1195
- 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}]`);
1196
1359
  found = true;
1197
1360
  count++;
1198
1361
  }
@@ -1220,12 +1383,14 @@ function sessionCommand() {
1220
1383
  }
1221
1384
  }
1222
1385
  }
1223
- searchDir(searchRoot);
1386
+ for (const { root, label } of searchRoots) {
1387
+ searchDir(root, label, root);
1388
+ }
1224
1389
  });
1225
1390
  session.command("ps").description("Show active Claude Code processes").action(() => {
1226
1391
  let files;
1227
1392
  try {
1228
- files = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1393
+ files = fs3.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1229
1394
  } catch {
1230
1395
  console.log("(no session files found)");
1231
1396
  return;
@@ -1239,7 +1404,7 @@ function sessionCommand() {
1239
1404
  console.log(fmt("---", "----------", "-------", "---", ""));
1240
1405
  for (const file of files) {
1241
1406
  try {
1242
- 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"));
1243
1408
  const pid = String(data.pid || "?");
1244
1409
  const sessionId = data.sessionId || "?";
1245
1410
  const cwd = data.cwd || "?";
@@ -1260,49 +1425,75 @@ function sessionCommand() {
1260
1425
  let nSessions = 0;
1261
1426
  let totalMsgs = 0;
1262
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
+ };
1263
1442
  try {
1264
- nProjects = fs2.readdirSync(PROJECTS_DIR).length;
1443
+ nProjects = fs3.readdirSync(PROJECTS_DIR).length;
1265
1444
  } catch {
1266
1445
  }
1267
1446
  try {
1268
- const walk = (dir) => {
1269
- const results = [];
1270
- try {
1271
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
1272
- const fullPath = path2.join(dir, entry.name);
1273
- if (entry.isDirectory()) results.push(...walk(fullPath));
1274
- else if (entry.name.endsWith(".jsonl")) results.push(fullPath);
1275
- }
1276
- } catch {
1277
- }
1278
- return results;
1279
- };
1280
1447
  const sessionFiles = walk(PROJECTS_DIR);
1281
1448
  nSessions = sessionFiles.length;
1282
1449
  for (const f of sessionFiles) {
1283
1450
  try {
1284
- const content = fs2.readFileSync(f, "utf-8");
1451
+ const content = fs3.readFileSync(f, "utf-8");
1285
1452
  totalMsgs += content ? content.split("\n").filter((l) => l.trim()).length : 0;
1286
1453
  } catch {
1287
1454
  }
1288
1455
  }
1289
1456
  } catch {
1290
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
+ }
1291
1472
  try {
1292
- nActive = fs2.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1473
+ nActive = fs3.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).length;
1293
1474
  } catch {
1294
1475
  }
1295
1476
  console.log(`Projects: ${nProjects}`);
1296
- console.log(`Sessions: ${nSessions}`);
1297
- 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
+ }
1298
1485
  console.log(`Active procs: ${nActive} (in ${SESSIONS_DIR})`);
1299
1486
  console.log("");
1300
1487
  try {
1301
- 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];
1302
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;
1303
1491
  console.log("Storage:");
1304
1492
  console.log(` Total: ${totalSize}`);
1305
1493
  console.log(` Projects: ${projSize}`);
1494
+ if (desktopSize) {
1495
+ console.log(` Desktop: ${desktopSize}`);
1496
+ }
1306
1497
  } catch {
1307
1498
  }
1308
1499
  });
@@ -1314,23 +1505,23 @@ function sessionCommand() {
1314
1505
  const walk = (dir) => {
1315
1506
  let entries;
1316
1507
  try {
1317
- entries = fs2.readdirSync(dir, { withFileTypes: true });
1508
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
1318
1509
  } catch {
1319
1510
  return;
1320
1511
  }
1321
1512
  for (const entry of entries) {
1322
- const fullPath = path2.join(dir, entry.name);
1513
+ const fullPath = path3.join(dir, entry.name);
1323
1514
  if (entry.isDirectory()) {
1324
1515
  walk(fullPath);
1325
1516
  } else if (entry.name.endsWith(".jsonl")) {
1326
1517
  try {
1327
- const stat = fs2.statSync(fullPath);
1518
+ const stat = fs3.statSync(fullPath);
1328
1519
  if (stat.mtimeMs < cutoffMs) {
1329
1520
  const size = stat.size;
1330
1521
  if (opts.dryRun) {
1331
1522
  console.log(`[dry-run] would delete: ${fullPath} (${Math.floor(size / 1024)}KB)`);
1332
1523
  } else {
1333
- fs2.unlinkSync(fullPath);
1524
+ fs3.unlinkSync(fullPath);
1334
1525
  console.log(`Deleted: ${fullPath}`);
1335
1526
  }
1336
1527
  deleted++;
@@ -1342,6 +1533,9 @@ function sessionCommand() {
1342
1533
  }
1343
1534
  };
1344
1535
  walk(PROJECTS_DIR);
1536
+ if (isDesktopAppInstalled()) {
1537
+ walk(DESKTOP_SESSIONS_DIR);
1538
+ }
1345
1539
  console.log("");
1346
1540
  const verb = opts.dryRun ? "Would delete" : "Deleted";
1347
1541
  console.log(`${verb} ${deleted} file(s) (~${Math.floor(freed / 1024)}KB freed)`);
@@ -1376,11 +1570,12 @@ _cc-hub() {
1376
1570
  profile_subcmds=(
1377
1571
  'add:Add or update a profile'
1378
1572
  'update:Update fields of an existing profile'
1379
- 'remove-model:Remove specific models from a profile'
1380
1573
  'list:List all profiles'
1381
1574
  'view:View full details of a profile'
1382
1575
  'remove:Remove a profile'
1576
+ 'rename:Rename a profile'
1383
1577
  'default:Set the default profile'
1578
+ 'sync:Synchronize all CLI profiles to the desktop app'
1384
1579
  )
1385
1580
 
1386
1581
  local -a hooks_subcmds
@@ -1434,8 +1629,12 @@ _cc-hub() {
1434
1629
  profile)
1435
1630
  if (( CURRENT == 2 )); then
1436
1631
  _describe -t profile-subcmds 'profile subcommand' profile_subcmds
1437
- elif [[ $words[2] == "view" || $words[2] == "remove" || $words[2] == "default" || $words[2] == "remove-model" ]]; then
1632
+ elif [[ $words[2] == "view" || $words[2] == "remove" || $words[2] == "default" ]]; then
1438
1633
  _cc_hub_profiles
1634
+ elif [[ $words[2] == "rename" ]]; then
1635
+ if (( CURRENT == 3 )); then
1636
+ _cc_hub_profiles
1637
+ fi
1439
1638
  elif [[ $words[2] == "update" ]]; then
1440
1639
  if (( CURRENT == 3 )); then
1441
1640
  _cc_hub_profiles
@@ -1520,7 +1719,7 @@ _cc-hub() {
1520
1719
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
1521
1720
  commands="profile use run hook session provider complete help"
1522
1721
 
1523
- local profile_subcmds="add update remove-model list view remove default"
1722
+ local profile_subcmds="add update list view remove rename default sync"
1524
1723
  local provider_subcmds="list"
1525
1724
  local provider_types="anthropic openai"
1526
1725
  local hooks_subcmds="list add remove enable disable"
@@ -1538,7 +1737,9 @@ _cc-hub() {
1538
1737
  profile)
1539
1738
  if [[ \${COMP_CWORD} -eq 2 ]]; then
1540
1739
  COMPREPLY=($(compgen -W "$profile_subcmds" -- "$cur"))
1541
- elif [[ "$prev" == "view" || "$prev" == "remove" || "$prev" == "default" || "$prev" == "remove-model" ]]; then
1740
+ elif [[ "$prev" == "view" || "$prev" == "remove" || "$prev" == "default" ]]; then
1741
+ _cc-hub_profiles
1742
+ elif [[ "$prev" == "rename" ]]; then
1542
1743
  _cc-hub_profiles
1543
1744
  elif [[ "$prev" == "profile" ]]; then
1544
1745
  COMPREPLY=($(compgen -W "$profile_subcmds" -- "$cur"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hub-cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Manage Claude CLI profiles, hooks, and sessions",
5
5
  "type": "module",
6
6
  "bin": {