claudeup 4.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.0.1",
3
+ "version": "4.2.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -11,15 +11,15 @@ export const cliTools = [
11
11
  packageName: "claudeup",
12
12
  },
13
13
  {
14
- name: "claudemem",
15
- displayName: "Claudemem",
16
- description: "Local semantic code search for Claude Code with vector embeddings",
17
- installCommand: "npm install -g claude-codemem",
18
- checkCommand: "claudemem --version",
19
- website: "https://github.com/MadAppGang/claudemem",
14
+ name: "mnemex",
15
+ displayName: "Mnemex",
16
+ description: "AST-aware code search with PageRank, callers/callees, and semantic embeddings",
17
+ installCommand: "npm install -g mnemex",
18
+ checkCommand: "mnemex --version",
19
+ website: "https://github.com/MadAppGang/mnemex",
20
20
  category: "ai-coding",
21
21
  packageManager: "npm",
22
- packageName: "claude-codemem",
22
+ packageName: "mnemex",
23
23
  },
24
24
  {
25
25
  name: "claudish",
@@ -18,23 +18,22 @@ export const cliTools: CliTool[] = [
18
18
  "TUI tool for managing Claude Code plugins, MCPs, and configuration",
19
19
  installCommand: "npm install -g claudeup",
20
20
  checkCommand: "claudeup --version",
21
- website:
22
- "https://github.com/MadAppGang/magus/tree/main/tools/claudeup",
21
+ website: "https://github.com/MadAppGang/magus/tree/main/tools/claudeup",
23
22
  category: "ai-coding",
24
23
  packageManager: "npm",
25
24
  packageName: "claudeup",
26
25
  },
27
26
  {
28
- name: "claudemem",
29
- displayName: "Claudemem",
27
+ name: "mnemex",
28
+ displayName: "Mnemex",
30
29
  description:
31
- "Local semantic code search for Claude Code with vector embeddings",
32
- installCommand: "npm install -g claude-codemem",
33
- checkCommand: "claudemem --version",
34
- website: "https://github.com/MadAppGang/claudemem",
30
+ "AST-aware code search with PageRank, callers/callees, and semantic embeddings",
31
+ installCommand: "npm install -g mnemex",
32
+ checkCommand: "mnemex --version",
33
+ website: "https://github.com/MadAppGang/mnemex",
35
34
  category: "ai-coding",
36
35
  packageManager: "npm",
37
- packageName: "claude-codemem",
36
+ packageName: "mnemex",
38
37
  },
39
38
  {
40
39
  name: "claudish",
@@ -70,7 +70,8 @@ export function getAllMarketplaces(localMarketplaces) {
70
70
  const canonical = deprecatedMarketplaces[name];
71
71
  if (canonical) {
72
72
  // If canonical already in the map or in defaults, skip this entry
73
- if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
73
+ if (all.has(canonical) ||
74
+ defaultMarketplaces.some((m) => m.name === canonical)) {
74
75
  continue;
75
76
  }
76
77
  }
@@ -85,7 +86,10 @@ export function getAllMarketplaces(localMarketplaces) {
85
86
  name,
86
87
  // Prefer default displayName over stale local clone data
87
88
  displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
88
- source: { source: "github", repo: defaultMp?.source.repo || local.gitRepo || "" },
89
+ source: {
90
+ source: "github",
91
+ repo: defaultMp?.source.repo || local.gitRepo || "",
92
+ },
89
93
  description: defaultMp?.description || local.description || "",
90
94
  official: defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
91
95
  featured: defaultMp?.featured,
@@ -85,7 +85,10 @@ export function getAllMarketplaces(
85
85
  const canonical = deprecatedMarketplaces[name];
86
86
  if (canonical) {
87
87
  // If canonical already in the map or in defaults, skip this entry
88
- if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
88
+ if (
89
+ all.has(canonical) ||
90
+ defaultMarketplaces.some((m) => m.name === canonical)
91
+ ) {
89
92
  continue;
90
93
  }
91
94
  }
@@ -100,8 +103,12 @@ export function getAllMarketplaces(
100
103
  all.set(name, {
101
104
  name,
102
105
  // Prefer default displayName over stale local clone data
103
- displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
104
- source: { source: "github" as const, repo: defaultMp?.source.repo || local.gitRepo || "" },
106
+ displayName:
107
+ defaultMp?.displayName || local.name || formatMarketplaceName(name),
108
+ source: {
109
+ source: "github" as const,
110
+ repo: defaultMp?.source.repo || local.gitRepo || "",
111
+ },
105
112
  description: defaultMp?.description || local.description || "",
106
113
  official:
107
114
  defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import { UpdateCache } from "../services/update-cache.js";
5
5
  import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
6
6
  import { runClaude } from "../services/claude-runner.js";
7
- import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, saveGlobalInstalledPluginVersion, } from "../services/claude-settings.js";
7
+ import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, saveGlobalInstalledPluginVersion, readGlobalSettings, writeGlobalSettings, } from "../services/claude-settings.js";
8
8
  import { parsePluginId } from "../utils/string-utils.js";
9
9
  import { defaultMarketplaces } from "../data/marketplaces.js";
10
10
  import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
@@ -22,20 +22,26 @@ async function getReferencedMarketplaces(projectPath) {
22
22
  for (const id of Object.keys(global))
23
23
  allPluginIds.add(id);
24
24
  }
25
- catch { /* skip if unreadable */ }
25
+ catch {
26
+ /* skip if unreadable */
27
+ }
26
28
  if (projectPath) {
27
29
  try {
28
30
  const project = await getEnabledPlugins(projectPath);
29
31
  for (const id of Object.keys(project))
30
32
  allPluginIds.add(id);
31
33
  }
32
- catch { /* skip if unreadable */ }
34
+ catch {
35
+ /* skip if unreadable */
36
+ }
33
37
  try {
34
38
  const local = await getLocalEnabledPlugins(projectPath);
35
39
  for (const id of Object.keys(local))
36
40
  allPluginIds.add(id);
37
41
  }
38
- catch { /* skip if unreadable */ }
42
+ catch {
43
+ /* skip if unreadable */
44
+ }
39
45
  }
40
46
  // Parse marketplace names from plugin IDs
41
47
  for (const pluginId of allPluginIds) {
@@ -73,6 +79,57 @@ async function autoAddMissingMarketplaces(projectPath) {
73
79
  }
74
80
  return added;
75
81
  }
82
+ const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
83
+ const CONTINUITY_PLUGIN_SCRIPT = path.join(os.homedir(), ".tmux", "plugins", "tmux-claude-continuity", "scripts", "on_session_start.sh");
84
+ /**
85
+ * Ensure tmux-claude-continuity Claude Code hooks are present in global settings.
86
+ * If the tmux plugin is installed but the hooks are missing, they are appended.
87
+ * Returns a description of what was added, or null if nothing changed.
88
+ */
89
+ async function ensureTmuxContinuityHooks() {
90
+ // Plugin not installed — nothing to do
91
+ if (!(await fs.pathExists(CONTINUITY_PLUGIN_SCRIPT))) {
92
+ return null;
93
+ }
94
+ const settings = await readGlobalSettings();
95
+ // Check if hooks are already configured by looking for the sentinel string
96
+ const existingHooks = settings.hooks ?? {};
97
+ for (const groups of Object.values(existingHooks)) {
98
+ for (const group of groups) {
99
+ for (const hook of group.hooks) {
100
+ if (hook.command.includes(CONTINUITY_PLUGIN_SENTINEL)) {
101
+ return null; // Already configured
102
+ }
103
+ }
104
+ }
105
+ }
106
+ // Append hooks, preserving any existing entries in SessionStart and Stop
107
+ const sessionStartGroups = existingHooks["SessionStart"] ?? [];
108
+ const stopGroups = existingHooks["Stop"] ?? [];
109
+ sessionStartGroups.push({
110
+ hooks: [
111
+ {
112
+ type: "command",
113
+ command: "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_session_start.sh",
114
+ },
115
+ ],
116
+ });
117
+ stopGroups.push({
118
+ hooks: [
119
+ {
120
+ type: "command",
121
+ command: "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_stop.sh",
122
+ },
123
+ ],
124
+ });
125
+ settings.hooks = {
126
+ ...existingHooks,
127
+ SessionStart: sessionStartGroups,
128
+ Stop: stopGroups,
129
+ };
130
+ await writeGlobalSettings(settings);
131
+ return "SessionStart + Stop hooks";
132
+ }
76
133
  /**
77
134
  * Prerun orchestration: Check for updates, apply them, then run claude
78
135
  * @param claudeArgs - Arguments to pass to claude CLI
@@ -84,9 +141,11 @@ export async function prerunClaude(claudeArgs, options = {}) {
84
141
  try {
85
142
  // STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
86
143
  const migration = await migrateMarketplaceRename();
87
- const migTotal = migration.projectMigrated + migration.globalMigrated
88
- + migration.localMigrated + migration.registryMigrated
89
- + (migration.knownMarketplacesMigrated ? 1 : 0);
144
+ const migTotal = migration.projectMigrated +
145
+ migration.globalMigrated +
146
+ migration.localMigrated +
147
+ migration.registryMigrated +
148
+ (migration.knownMarketplacesMigrated ? 1 : 0);
90
149
  if (migTotal > 0) {
91
150
  console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
92
151
  }
@@ -99,6 +158,11 @@ export async function prerunClaude(claudeArgs, options = {}) {
99
158
  console.log(`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`);
100
159
  }
101
160
  }
161
+ // STEP 0.6: Ensure tmux-claude-continuity hooks are configured
162
+ const addedHooks = await ensureTmuxContinuityHooks();
163
+ if (addedHooks) {
164
+ console.log(`✓ Added tmux-claude-continuity hooks to ~/.claude/settings.json`);
165
+ }
102
166
  // STEP 1: Check if we should update (time-based cache, or forced)
103
167
  const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
104
168
  if (options.force) {
@@ -14,6 +14,8 @@ import {
14
14
  getEnabledPlugins,
15
15
  getLocalEnabledPlugins,
16
16
  saveGlobalInstalledPluginVersion,
17
+ readGlobalSettings,
18
+ writeGlobalSettings,
17
19
  } from "../services/claude-settings.js";
18
20
  import { parsePluginId } from "../utils/string-utils.js";
19
21
  import { defaultMarketplaces } from "../data/marketplaces.js";
@@ -49,18 +51,24 @@ async function getReferencedMarketplaces(
49
51
  try {
50
52
  const global = await getGlobalEnabledPlugins();
51
53
  for (const id of Object.keys(global)) allPluginIds.add(id);
52
- } catch { /* skip if unreadable */ }
54
+ } catch {
55
+ /* skip if unreadable */
56
+ }
53
57
 
54
58
  if (projectPath) {
55
59
  try {
56
60
  const project = await getEnabledPlugins(projectPath);
57
61
  for (const id of Object.keys(project)) allPluginIds.add(id);
58
- } catch { /* skip if unreadable */ }
62
+ } catch {
63
+ /* skip if unreadable */
64
+ }
59
65
 
60
66
  try {
61
67
  const local = await getLocalEnabledPlugins(projectPath);
62
68
  for (const id of Object.keys(local)) allPluginIds.add(id);
63
- } catch { /* skip if unreadable */ }
69
+ } catch {
70
+ /* skip if unreadable */
71
+ }
64
72
  }
65
73
 
66
74
  // Parse marketplace names from plugin IDs
@@ -108,6 +116,75 @@ async function autoAddMissingMarketplaces(
108
116
  return added;
109
117
  }
110
118
 
119
+ const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
120
+ const CONTINUITY_PLUGIN_SCRIPT = path.join(
121
+ os.homedir(),
122
+ ".tmux",
123
+ "plugins",
124
+ "tmux-claude-continuity",
125
+ "scripts",
126
+ "on_session_start.sh",
127
+ );
128
+
129
+ /**
130
+ * Ensure tmux-claude-continuity Claude Code hooks are present in global settings.
131
+ * If the tmux plugin is installed but the hooks are missing, they are appended.
132
+ * Returns a description of what was added, or null if nothing changed.
133
+ */
134
+ async function ensureTmuxContinuityHooks(): Promise<string | null> {
135
+ // Plugin not installed — nothing to do
136
+ if (!(await fs.pathExists(CONTINUITY_PLUGIN_SCRIPT))) {
137
+ return null;
138
+ }
139
+
140
+ const settings = await readGlobalSettings();
141
+
142
+ // Check if hooks are already configured by looking for the sentinel string
143
+ const existingHooks = settings.hooks ?? {};
144
+ for (const groups of Object.values(existingHooks)) {
145
+ for (const group of groups) {
146
+ for (const hook of group.hooks) {
147
+ if (hook.command.includes(CONTINUITY_PLUGIN_SENTINEL)) {
148
+ return null; // Already configured
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Append hooks, preserving any existing entries in SessionStart and Stop
155
+ const sessionStartGroups = existingHooks["SessionStart"] ?? [];
156
+ const stopGroups = existingHooks["Stop"] ?? [];
157
+
158
+ sessionStartGroups.push({
159
+ hooks: [
160
+ {
161
+ type: "command",
162
+ command:
163
+ "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_session_start.sh",
164
+ },
165
+ ],
166
+ });
167
+
168
+ stopGroups.push({
169
+ hooks: [
170
+ {
171
+ type: "command",
172
+ command:
173
+ "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_stop.sh",
174
+ },
175
+ ],
176
+ });
177
+
178
+ settings.hooks = {
179
+ ...existingHooks,
180
+ SessionStart: sessionStartGroups,
181
+ Stop: stopGroups,
182
+ };
183
+
184
+ await writeGlobalSettings(settings);
185
+ return "SessionStart + Stop hooks";
186
+ }
187
+
111
188
  /**
112
189
  * Prerun orchestration: Check for updates, apply them, then run claude
113
190
  * @param claudeArgs - Arguments to pass to claude CLI
@@ -123,9 +200,12 @@ export async function prerunClaude(
123
200
  try {
124
201
  // STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
125
202
  const migration = await migrateMarketplaceRename();
126
- const migTotal = migration.projectMigrated + migration.globalMigrated
127
- + migration.localMigrated + migration.registryMigrated
128
- + (migration.knownMarketplacesMigrated ? 1 : 0);
203
+ const migTotal =
204
+ migration.projectMigrated +
205
+ migration.globalMigrated +
206
+ migration.localMigrated +
207
+ migration.registryMigrated +
208
+ (migration.knownMarketplacesMigrated ? 1 : 0);
129
209
  if (migTotal > 0) {
130
210
  console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
131
211
  }
@@ -142,6 +222,14 @@ export async function prerunClaude(
142
222
  }
143
223
  }
144
224
 
225
+ // STEP 0.6: Ensure tmux-claude-continuity hooks are configured
226
+ const addedHooks = await ensureTmuxContinuityHooks();
227
+ if (addedHooks) {
228
+ console.log(
229
+ `✓ Added tmux-claude-continuity hooks to ~/.claude/settings.json`,
230
+ );
231
+ }
232
+
145
233
  // STEP 1: Check if we should update (time-based cache, or forced)
146
234
  const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
147
235
 
@@ -418,6 +418,8 @@ export async function getMarketplaceAutoUpdate(marketplaceName) {
418
418
  // =============================================================================
419
419
  const OLD_MARKETPLACE_NAMES = ["mag-claude-plugins", "MadAppGang-claude-code"];
420
420
  const NEW_MARKETPLACE_NAME = "magus";
421
+ const NEW_MARKETPLACE_REPO = "MadAppGang/magus";
422
+ const OLD_MARKETPLACE_REPOS = ["MadAppGang/claude-code"];
421
423
  /**
422
424
  * Rename plugin keys in a Record from any old marketplace name to new.
423
425
  * e.g., "frontend@mag-claude-plugins" → "frontend@magus"
@@ -468,11 +470,24 @@ function migrateSettingsObject(settings) {
468
470
  const entry = settings.extraKnownMarketplaces[oldName];
469
471
  delete settings.extraKnownMarketplaces[oldName];
470
472
  if (!settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME]) {
471
- settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = entry;
473
+ settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = {
474
+ ...entry,
475
+ source: {
476
+ ...entry.source,
477
+ repo: NEW_MARKETPLACE_REPO,
478
+ },
479
+ };
472
480
  }
473
481
  total++;
474
482
  }
475
483
  }
484
+ // Fix stale repo URL on existing magus entry (e.g. key is "magus" but repo is still "MadAppGang/claude-code")
485
+ const magusEntry = settings.extraKnownMarketplaces?.[NEW_MARKETPLACE_NAME];
486
+ if (magusEntry?.source?.repo &&
487
+ OLD_MARKETPLACE_REPOS.includes(magusEntry.source.repo)) {
488
+ magusEntry.source.repo = NEW_MARKETPLACE_REPO;
489
+ total++;
490
+ }
476
491
  return total;
477
492
  }
478
493
  /**
@@ -498,7 +513,9 @@ export async function migrateMarketplaceRename(projectPath) {
498
513
  result.projectMigrated = count;
499
514
  }
500
515
  }
501
- catch { /* skip if unreadable */ }
516
+ catch {
517
+ /* skip if unreadable */
518
+ }
502
519
  // 2. Global settings
503
520
  try {
504
521
  const settings = await readGlobalSettings();
@@ -508,7 +525,9 @@ export async function migrateMarketplaceRename(projectPath) {
508
525
  result.globalMigrated = count;
509
526
  }
510
527
  }
511
- catch { /* skip if unreadable */ }
528
+ catch {
529
+ /* skip if unreadable */
530
+ }
512
531
  // 3. Local settings (settings.local.json)
513
532
  try {
514
533
  const local = await readLocalSettings(projectPath);
@@ -528,7 +547,9 @@ export async function migrateMarketplaceRename(projectPath) {
528
547
  result.localMigrated = localCount;
529
548
  }
530
549
  }
531
- catch { /* skip if unreadable */ }
550
+ catch {
551
+ /* skip if unreadable */
552
+ }
532
553
  // 4. known_marketplaces.json — rename old keys + physical directory cleanup
533
554
  const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
534
555
  const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
@@ -553,8 +574,7 @@ export async function migrateMarketplaceRename(projectPath) {
553
574
  }
554
575
  // Ensure installLocation doesn't reference old directory names
555
576
  if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
556
- known[NEW_MARKETPLACE_NAME].installLocation =
557
- known[NEW_MARKETPLACE_NAME].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
577
+ known[NEW_MARKETPLACE_NAME].installLocation = known[NEW_MARKETPLACE_NAME].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
558
578
  knownModified = true;
559
579
  }
560
580
  }
@@ -563,7 +583,9 @@ export async function migrateMarketplaceRename(projectPath) {
563
583
  result.knownMarketplacesMigrated = true;
564
584
  }
565
585
  }
566
- catch { /* skip if unreadable */ }
586
+ catch {
587
+ /* skip if unreadable */
588
+ }
567
589
  // 4b. Rename/remove old physical directories (runs even if keys were already migrated)
568
590
  for (const oldName of OLD_MARKETPLACE_NAMES) {
569
591
  const oldDir = path.join(pluginsDir, oldName);
@@ -578,24 +600,32 @@ export async function migrateMarketplaceRename(projectPath) {
578
600
  }
579
601
  }
580
602
  }
581
- catch { /* non-fatal: directory cleanup is best-effort */ }
603
+ catch {
604
+ /* non-fatal: directory cleanup is best-effort */
605
+ }
582
606
  }
583
607
  // 4c. Update git remote URL in the marketplace clone (old → new repo)
584
608
  try {
585
609
  if (await fs.pathExists(path.join(newDir, ".git"))) {
586
610
  const { execSync } = await import("node:child_process");
587
611
  const remote = execSync("git remote get-url origin", {
588
- cwd: newDir, encoding: "utf-8", timeout: 5000,
612
+ cwd: newDir,
613
+ encoding: "utf-8",
614
+ timeout: 5000,
589
615
  }).trim();
590
616
  if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
591
617
  const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
592
618
  execSync(`git remote set-url origin "${newRemote}"`, {
593
- cwd: newDir, encoding: "utf-8", timeout: 5000,
619
+ cwd: newDir,
620
+ encoding: "utf-8",
621
+ timeout: 5000,
594
622
  });
595
623
  }
596
624
  }
597
625
  }
598
- catch { /* non-fatal: git remote update is best-effort */ }
626
+ catch {
627
+ /* non-fatal: git remote update is best-effort */
628
+ }
599
629
  // 5. installed_plugins.json — rename plugin ID keys
600
630
  try {
601
631
  const registry = await readInstalledPluginsRegistry();
@@ -621,7 +651,44 @@ export async function migrateMarketplaceRename(projectPath) {
621
651
  result.registryMigrated = regCount;
622
652
  }
623
653
  }
624
- catch { /* skip if unreadable */ }
654
+ catch {
655
+ /* skip if unreadable */
656
+ }
657
+ // 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
658
+ try {
659
+ const projectsDir = path.join(os.homedir(), ".claude", "projects");
660
+ if (await fs.pathExists(projectsDir)) {
661
+ const entries = await fs.readdir(projectsDir);
662
+ const seenPaths = new Set();
663
+ // Current project (from step 1) already handled — skip it
664
+ const currentProject = projectPath || process.cwd();
665
+ seenPaths.add(currentProject);
666
+ for (const entry of entries) {
667
+ // Directory names encode paths: -Users-jack-dev-foo → /Users/jack/dev/foo
668
+ const decoded = entry.replace(/^-/, "/").replace(/-/g, "/");
669
+ if (seenPaths.has(decoded))
670
+ continue;
671
+ seenPaths.add(decoded);
672
+ const settingsFile = path.join(decoded, ".claude", "settings.json");
673
+ try {
674
+ if (await fs.pathExists(settingsFile)) {
675
+ const raw = await fs.readJson(settingsFile);
676
+ const count = migrateSettingsObject(raw);
677
+ if (count > 0) {
678
+ await fs.writeJson(settingsFile, raw, { spaces: 2 });
679
+ result.projectMigrated += count;
680
+ }
681
+ }
682
+ }
683
+ catch {
684
+ /* skip individual projects that fail */
685
+ }
686
+ }
687
+ }
688
+ }
689
+ catch {
690
+ /* non-fatal: cross-project scan is best-effort */
691
+ }
625
692
  return result;
626
693
  }
627
694
  /**