sync-worktrees 3.0.1 → 3.1.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.
@@ -85,6 +85,11 @@ var PATH_CONSTANTS = {
85
85
  GIT_DIR: ".git",
86
86
  README: "README"
87
87
  };
88
+ var CONFIG_FILE_NAMES = [
89
+ "sync-worktrees.config.js",
90
+ "sync-worktrees.config.mjs",
91
+ "sync-worktrees.config.cjs"
92
+ ];
88
93
  var METADATA_CONSTANTS = {
89
94
  MAX_HISTORY_ENTRIES: 10,
90
95
  METADATA_FILENAME: "sync-metadata.json",
@@ -124,6 +129,24 @@ function filterBranchesByName(branches, include, exclude) {
124
129
 
125
130
  // src/services/config-loader.service.ts
126
131
  var ConfigLoaderService = class {
132
+ async findConfigUpward(startDir) {
133
+ let current = path.resolve(startDir);
134
+ const root = path.parse(current).root;
135
+ while (true) {
136
+ for (const name of CONFIG_FILE_NAMES) {
137
+ const candidate = path.join(current, name);
138
+ try {
139
+ await fs.access(candidate);
140
+ return candidate;
141
+ } catch {
142
+ }
143
+ }
144
+ if (current === root) return null;
145
+ const parent = path.dirname(current);
146
+ if (parent === current) return null;
147
+ current = parent;
148
+ }
149
+ }
127
150
  async loadConfigFile(configPath) {
128
151
  const absolutePath = path.resolve(configPath);
129
152
  try {
@@ -1173,6 +1196,7 @@ var WorktreeStatusService = class {
1173
1196
  if (hasUnpushedCommits) reasons.push("unpushed commits");
1174
1197
  if (hasOperationInProgress) reasons.push("operation in progress");
1175
1198
  if (hasModifiedSubmodules) reasons.push("modified submodules");
1199
+ if (upstreamGone) reasons.push("upstream gone");
1176
1200
  const canRemove = isClean && !hasUnpushedCommits && !hasOperationInProgress && !hasModifiedSubmodules;
1177
1201
  const details = includeDetails ? this.buildStatusDetails(snap) : void 0;
1178
1202
  return {
@@ -2804,15 +2828,18 @@ var WorktreeSyncService = class {
2804
2828
  // src/mcp/context.ts
2805
2829
  var AUTO_DETECT_PREFIX = "__auto_detected__:";
2806
2830
  var DISCOVERY_CACHE_TTL_MS = 5e3;
2807
- var EMPTY_CAPABILITIES = {
2808
- canListWorktrees: false,
2809
- canGetStatus: false,
2810
- canCreateWorktree: false,
2811
- canRemoveWorktree: false,
2812
- canUpdateWorktree: false,
2813
- canSync: false,
2814
- canInitialize: false
2815
- };
2831
+ function emptyCapabilities(reason) {
2832
+ const state = reason ? { available: false, reason } : { available: false };
2833
+ return {
2834
+ listWorktrees: { ...state },
2835
+ getStatus: { ...state },
2836
+ createWorktree: { ...state },
2837
+ removeWorktree: { ...state },
2838
+ updateWorktree: { ...state },
2839
+ sync: { ...state },
2840
+ initialize: { ...state }
2841
+ };
2842
+ }
2816
2843
  function buildUnsupportedContext(currentPath, reason) {
2817
2844
  return {
2818
2845
  isWorktree: false,
@@ -2823,10 +2850,11 @@ function buildUnsupportedContext(currentPath, reason) {
2823
2850
  repoUrl: null,
2824
2851
  worktreeDir: null,
2825
2852
  allWorktrees: [],
2826
- configLoaded: false,
2853
+ siblingRepositories: [],
2854
+ configPath: null,
2827
2855
  repoName: null,
2828
- capabilities: EMPTY_CAPABILITIES,
2829
- reasons: [reason]
2856
+ capabilities: emptyCapabilities(reason),
2857
+ notes: [reason]
2830
2858
  };
2831
2859
  }
2832
2860
  function createStderrLogger(repoName) {
@@ -2843,7 +2871,8 @@ var RepositoryContext = class {
2843
2871
  configPath = null;
2844
2872
  configLoader = new ConfigLoaderService();
2845
2873
  discoveryCache = /* @__PURE__ */ new Map();
2846
- async loadConfig(configPath) {
2874
+ async loadConfig(configPath, options = {}) {
2875
+ const setDefaultCurrent = options.setDefaultCurrent ?? true;
2847
2876
  const absolutePath = path8.resolve(configPath);
2848
2877
  const configFile = await this.configLoader.loadConfigFile(absolutePath);
2849
2878
  for (const [name, entry] of this.repos) {
@@ -2865,9 +2894,10 @@ var RepositoryContext = class {
2865
2894
  if (this.currentRepo && !this.repos.has(this.currentRepo)) {
2866
2895
  this.currentRepo = null;
2867
2896
  }
2868
- if (!this.currentRepo && configFile.repositories.length > 0) {
2897
+ if (setDefaultCurrent && !this.currentRepo && configFile.repositories.length > 0) {
2869
2898
  this.currentRepo = configFile.repositories[0].name;
2870
2899
  }
2900
+ this.discoveryCache.clear();
2871
2901
  return configFile.repositories;
2872
2902
  }
2873
2903
  async detectFromPath(dirPath) {
@@ -2876,6 +2906,17 @@ var RepositoryContext = class {
2876
2906
  if (cached && await this.isCacheFresh(cached)) {
2877
2907
  return cached.result;
2878
2908
  }
2909
+ if (this.configPath === null) {
2910
+ const found = await this.configLoader.findConfigUpward(absolutePath);
2911
+ if (found) {
2912
+ try {
2913
+ await this.loadConfig(found, { setDefaultCurrent: false });
2914
+ } catch (err) {
2915
+ process.stderr.write(`[sync-worktrees] auto-loaded config failed: ${err.message}
2916
+ `);
2917
+ }
2918
+ }
2919
+ }
2879
2920
  const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
2880
2921
  if (result.isWorktree && result.bareRepoPath && adminDir) {
2881
2922
  const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
@@ -2911,10 +2952,49 @@ var RepositoryContext = class {
2911
2952
  __discoveryCacheSizeForTest() {
2912
2953
  return this.discoveryCache.size;
2913
2954
  }
2914
- bootstrapCurrentRepo(candidate) {
2955
+ async discoverSiblingRepositories(currentBareRepoPath) {
2956
+ const repoDir = path8.dirname(currentBareRepoPath);
2957
+ const workspaceRoot = path8.dirname(repoDir);
2958
+ if (workspaceRoot === repoDir) return [];
2959
+ let entries;
2960
+ try {
2961
+ entries = await fs7.readdir(workspaceRoot);
2962
+ } catch {
2963
+ return [];
2964
+ }
2965
+ const configBares = /* @__PURE__ */ new Map();
2966
+ for (const entry of this.repos.values()) {
2967
+ if (entry.source === "config" && entry.config.bareRepoDir) {
2968
+ configBares.set(normalizePathForCompare(entry.config.bareRepoDir), entry.name);
2969
+ }
2970
+ }
2971
+ const results = [];
2972
+ await Promise.all(
2973
+ entries.map(async (entry) => {
2974
+ const candidate = path8.join(workspaceRoot, entry);
2975
+ const bareCandidate = path8.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
2976
+ try {
2977
+ const stat4 = await fs7.stat(bareCandidate);
2978
+ if (!stat4.isDirectory()) return;
2979
+ } catch {
2980
+ return;
2981
+ }
2982
+ const resolvedBare = path8.resolve(bareCandidate);
2983
+ const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
2984
+ results.push({
2985
+ name: matchedName ?? entry,
2986
+ bareRepoPath: resolvedBare,
2987
+ configMatched: matchedName !== void 0
2988
+ });
2989
+ })
2990
+ );
2991
+ results.sort((a, b) => a.name.localeCompare(b.name));
2992
+ return results;
2993
+ }
2994
+ bootstrapCurrentRepo(candidate, force = false) {
2915
2995
  if (this.currentRepo !== null) return;
2916
2996
  if (!this.repos.has(candidate)) return;
2917
- if (this.repos.size !== 1) return;
2997
+ if (!force && this.repos.size !== 1) return;
2918
2998
  this.currentRepo = candidate;
2919
2999
  }
2920
3000
  async isCacheFresh(cached) {
@@ -2927,14 +3007,14 @@ var RepositoryContext = class {
2927
3007
  return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
2928
3008
  }
2929
3009
  async detectFromPathUncached(absolutePath) {
2930
- const reasons = [];
3010
+ const notes = [];
2931
3011
  const located = await findWorktreeRoot(absolutePath);
2932
3012
  const worktreeRoot = located?.worktreeRoot ?? absolutePath;
2933
- const unsupported = (reason, isWorktree = false) => {
2934
- reasons.push(reason);
3013
+ const unsupported = (reason) => {
3014
+ notes.push(reason);
2935
3015
  return {
2936
3016
  result: {
2937
- isWorktree,
3017
+ isWorktree: false,
2938
3018
  kind: "unsupported",
2939
3019
  currentBranch: null,
2940
3020
  currentWorktreePath: worktreeRoot,
@@ -2942,10 +3022,11 @@ var RepositoryContext = class {
2942
3022
  repoUrl: null,
2943
3023
  worktreeDir: null,
2944
3024
  allWorktrees: [],
2945
- configLoaded: this.configPath !== null,
3025
+ siblingRepositories: [],
3026
+ configPath: this.configPath,
2946
3027
  repoName: null,
2947
- capabilities: EMPTY_CAPABILITIES,
2948
- reasons
3028
+ capabilities: emptyCapabilities(reason),
3029
+ notes
2949
3030
  },
2950
3031
  adminDir: null
2951
3032
  };
@@ -2979,7 +3060,7 @@ var RepositoryContext = class {
2979
3060
  const urlStr = typeof remoteResult === "string" ? remoteResult.trim() : "";
2980
3061
  repoUrl = urlStr || null;
2981
3062
  } catch {
2982
- reasons.push("Could not read remote origin URL");
3063
+ notes.push("Could not read remote origin URL");
2983
3064
  }
2984
3065
  const listOutput = await bareGit.raw(["worktree", "list", "--porcelain"]);
2985
3066
  worktrees = parseWorktreeList(listOutput, worktreeRoot);
@@ -2988,7 +3069,8 @@ var RepositoryContext = class {
2988
3069
  currentBranch = current.branch;
2989
3070
  }
2990
3071
  } catch (err) {
2991
- reasons.push(`Failed to read bare repo at ${bareRepoPath}: ${err.message}`);
3072
+ const reason = `Failed to read bare repo at ${bareRepoPath}: ${err.message}`;
3073
+ notes.push(reason);
2992
3074
  return {
2993
3075
  result: {
2994
3076
  isWorktree: true,
@@ -2999,34 +3081,31 @@ var RepositoryContext = class {
2999
3081
  repoUrl: null,
3000
3082
  worktreeDir: null,
3001
3083
  allWorktrees: [],
3002
- configLoaded: this.configPath !== null,
3084
+ siblingRepositories: [],
3085
+ configPath: this.configPath,
3003
3086
  repoName: null,
3004
- capabilities: EMPTY_CAPABILITIES,
3005
- reasons
3087
+ capabilities: emptyCapabilities(reason),
3088
+ notes
3006
3089
  },
3007
3090
  adminDir
3008
3091
  };
3009
3092
  }
3010
3093
  const worktreeDir = path8.dirname(worktreeRoot);
3094
+ const noUrlReason = "no remote origin URL detected";
3011
3095
  const capabilities = {
3012
- canListWorktrees: true,
3013
- canGetStatus: true,
3014
- canCreateWorktree: repoUrl !== null,
3015
- canRemoveWorktree: true,
3016
- canUpdateWorktree: true,
3017
- canSync: false,
3018
- canInitialize: false
3096
+ listWorktrees: { available: true },
3097
+ getStatus: { available: true },
3098
+ createWorktree: repoUrl !== null ? { available: true } : { available: false, reason: noUrlReason },
3099
+ removeWorktree: { available: true },
3100
+ updateWorktree: { available: true },
3101
+ sync: { available: false, reason: "no config and no remote URL" },
3102
+ initialize: { available: false, reason: "no config and no remote URL" }
3019
3103
  };
3020
- if (!repoUrl) {
3021
- reasons.push("create_worktree unavailable: no remote origin URL detected");
3022
- }
3023
- const foldPath = (p) => isCaseInsensitiveFs() ? p.toLowerCase() : p;
3024
- const foldedBare = foldPath(bareRepoPath);
3104
+ const foldedBare = normalizePathForCompare(bareRepoPath);
3025
3105
  let matchedConfig = null;
3026
3106
  for (const entry of this.repos.values()) {
3027
- if (entry.source === "config") {
3028
- const entryBare = entry.config.bareRepoDir ? path8.resolve(entry.config.bareRepoDir) : null;
3029
- if (entryBare && foldPath(entryBare) === foldedBare) {
3107
+ if (entry.source === "config" && entry.config.bareRepoDir) {
3108
+ if (normalizePathForCompare(entry.config.bareRepoDir) === foldedBare) {
3030
3109
  matchedConfig = entry;
3031
3110
  break;
3032
3111
  }
@@ -3037,8 +3116,8 @@ var RepositoryContext = class {
3037
3116
  if (matchedConfig) {
3038
3117
  repoName = matchedConfig.name;
3039
3118
  kind = "managed";
3040
- capabilities.canSync = true;
3041
- capabilities.canInitialize = true;
3119
+ capabilities.sync = { available: true };
3120
+ capabilities.initialize = { available: true };
3042
3121
  } else if (repoUrl) {
3043
3122
  const syntheticConfig = {
3044
3123
  repoUrl,
@@ -3056,13 +3135,14 @@ var RepositoryContext = class {
3056
3135
  });
3057
3136
  }
3058
3137
  repoName = detectedKey;
3059
- reasons.push("sync/initialize unavailable: no config file loaded (running in auto-detect mode)");
3060
- } else {
3061
- reasons.push("sync/initialize unavailable: no config file and no remote URL");
3138
+ const autoReason = "no config file loaded (running in auto-detect mode)";
3139
+ capabilities.sync = { available: false, reason: autoReason };
3140
+ capabilities.initialize = { available: false, reason: autoReason };
3062
3141
  }
3063
3142
  if (repoName) {
3064
- this.bootstrapCurrentRepo(repoName);
3143
+ this.bootstrapCurrentRepo(repoName, matchedConfig !== null);
3065
3144
  }
3145
+ const siblingRepositories = await this.discoverSiblingRepositories(bareRepoPath);
3066
3146
  const discovered = {
3067
3147
  isWorktree: true,
3068
3148
  kind,
@@ -3072,10 +3152,11 @@ var RepositoryContext = class {
3072
3152
  repoUrl,
3073
3153
  worktreeDir,
3074
3154
  allWorktrees: worktrees,
3075
- configLoaded: this.configPath !== null,
3155
+ siblingRepositories,
3156
+ configPath: this.configPath,
3076
3157
  repoName,
3077
3158
  capabilities,
3078
- reasons
3159
+ notes
3079
3160
  };
3080
3161
  if (repoName) {
3081
3162
  const entry = this.repos.get(repoName);
@@ -3134,9 +3215,7 @@ var RepositoryContext = class {
3134
3215
  }
3135
3216
  };
3136
3217
  function parseWorktreeList(output, currentPath) {
3137
- const resolvedCurrent = path8.resolve(currentPath);
3138
- const fold = (p) => isCaseInsensitiveFs() ? p.toLowerCase() : p;
3139
- const foldedCurrent = fold(resolvedCurrent);
3218
+ const foldedCurrent = normalizePathForCompare(currentPath);
3140
3219
  const results = [];
3141
3220
  for (const wt of parseWorktreeListPorcelain(output)) {
3142
3221
  const resolved = path8.resolve(wt.path);
@@ -3145,7 +3224,7 @@ function parseWorktreeList(output, currentPath) {
3145
3224
  results.push({
3146
3225
  path: resolved,
3147
3226
  branch,
3148
- isCurrent: fold(resolved) === foldedCurrent
3227
+ isCurrent: normalizePathForCompare(resolved) === foldedCurrent
3149
3228
  });
3150
3229
  }
3151
3230
  return results;
@@ -3189,7 +3268,24 @@ import { z } from "zod";
3189
3268
  // src/mcp/handlers.ts
3190
3269
  import * as path9 from "path";
3191
3270
  import pLimit2 from "p-limit";
3192
- import simpleGit5 from "simple-git";
3271
+
3272
+ // src/utils/disk-space.ts
3273
+ import fastFolderSize from "fast-folder-size";
3274
+ async function calculateDirectorySize(dirPath) {
3275
+ return new Promise((resolve9, reject) => {
3276
+ fastFolderSize(dirPath, (err, bytes) => {
3277
+ if (err) {
3278
+ reject(err);
3279
+ return;
3280
+ }
3281
+ if (bytes === void 0) {
3282
+ reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
3283
+ return;
3284
+ }
3285
+ resolve9(bytes);
3286
+ });
3287
+ });
3288
+ }
3193
3289
 
3194
3290
  // src/utils/git-validation.ts
3195
3291
  function isValidGitBranchName(name) {
@@ -3299,12 +3395,45 @@ function wrapHandler(fn) {
3299
3395
  };
3300
3396
  }
3301
3397
 
3398
+ // src/mcp/worktree-summary.ts
3399
+ import simpleGit5 from "simple-git";
3400
+ function deriveLabel(status, isCurrent) {
3401
+ if (isCurrent) return "current";
3402
+ if (!status.isClean || status.hasUnpushedCommits || status.hasStashedChanges) return "dirty";
3403
+ if (status.upstreamGone) return "stale";
3404
+ return "clean";
3405
+ }
3406
+ function deriveSafeToRemove(status) {
3407
+ if (status.canRemove && !status.upstreamGone) {
3408
+ return { safe: true, reason: "clean tree, no unpushed commits" };
3409
+ }
3410
+ if (status.canRemove && status.upstreamGone) {
3411
+ return { safe: false, reason: "branch deleted upstream \u2014 verify no work is lost before removal" };
3412
+ }
3413
+ if (status.reasons.length > 0) {
3414
+ return { safe: false, reason: status.reasons.join(", ") };
3415
+ }
3416
+ return { safe: false, reason: "not safe to remove" };
3417
+ }
3418
+ async function getDivergence(worktreePath) {
3419
+ try {
3420
+ const git = simpleGit5(worktreePath);
3421
+ const output = await git.raw(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
3422
+ const [aheadStr, behindStr] = output.trim().split(/\s+/);
3423
+ return { ahead: parseInt(aheadStr, 10), behind: parseInt(behindStr, 10) };
3424
+ } catch {
3425
+ return null;
3426
+ }
3427
+ }
3428
+
3302
3429
  // src/mcp/handlers.ts
3303
3430
  var pathResolution = new PathResolutionService();
3304
3431
  function ensureCapability(discovered, key, toolName) {
3305
3432
  if (!discovered) return;
3306
- if (!discovered.capabilities[key]) {
3307
- throw new CapabilityUnavailableError(toolName, discovered.reasons);
3433
+ const cap = discovered.capabilities[key];
3434
+ if (!cap.available) {
3435
+ const reasons = cap.reason ? [cap.reason] : discovered.notes;
3436
+ throw new CapabilityUnavailableError(toolName, reasons);
3308
3437
  }
3309
3438
  }
3310
3439
  async function ensureNotSyncing(ctx, repoName) {
@@ -3349,30 +3478,35 @@ async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
3349
3478
  }
3350
3479
  throw new Error(`Path '${targetPath}' is not a registered worktree of the current repository`);
3351
3480
  }
3352
- function deriveLabel(status, isCurrent) {
3353
- if (isCurrent) return "current";
3354
- if (!status.isClean || status.hasUnpushedCommits || status.hasStashedChanges) return "dirty";
3355
- if (status.upstreamGone) return "stale";
3356
- return "clean";
3357
- }
3358
- async function getDivergence(worktreePath) {
3359
- try {
3360
- const git = simpleGit5(worktreePath);
3361
- const output = await git.raw(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
3362
- const [aheadStr, behindStr] = output.trim().split(/\s+/);
3363
- return { ahead: parseInt(aheadStr, 10), behind: parseInt(behindStr, 10) };
3364
- } catch {
3365
- return null;
3366
- }
3367
- }
3368
3481
  async function handleDetectContext(ctx, params, _extra) {
3369
3482
  const target = params.path ?? process.cwd();
3370
3483
  const discovered = await ctx.detectFromPath(target);
3371
- return formatToolResponse(discovered);
3484
+ if (!params.includeStatus || discovered.allWorktrees.length === 0) {
3485
+ return formatToolResponse(discovered);
3486
+ }
3487
+ const statusService = new WorktreeStatusService();
3488
+ const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
3489
+ const enriched = await Promise.all(
3490
+ discovered.allWorktrees.map(
3491
+ (wt) => limit(async () => {
3492
+ const [status, divergence] = await Promise.all([
3493
+ statusService.getFullWorktreeStatus(wt.path, false).catch(() => null),
3494
+ getDivergence(wt.path)
3495
+ ]);
3496
+ return {
3497
+ ...wt,
3498
+ label: status ? deriveLabel(status, wt.isCurrent) : wt.isCurrent ? "current" : "unknown",
3499
+ divergence,
3500
+ staleHint: status?.upstreamGone ?? false
3501
+ };
3502
+ })
3503
+ )
3504
+ );
3505
+ return formatToolResponse({ ...discovered, allWorktrees: enriched });
3372
3506
  }
3373
3507
  async function handleListWorktrees(ctx, params, _extra) {
3374
3508
  const { discovered, git } = await getReadyService(ctx, params.repoName, {
3375
- capability: "canListWorktrees",
3509
+ capability: "listWorktrees",
3376
3510
  toolName: "list_worktrees"
3377
3511
  });
3378
3512
  let worktrees;
@@ -3392,20 +3526,22 @@ async function handleListWorktrees(ctx, params, _extra) {
3392
3526
  (wt) => limit(async () => {
3393
3527
  const resolvedPath = path9.resolve(wt.path);
3394
3528
  const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
3395
- const [status, divergence, metadata] = await Promise.all([
3529
+ const [status, divergence, metadata, sizeBytes] = await Promise.all([
3396
3530
  git.getFullWorktreeStatus(wt.path, false).catch(() => null),
3397
3531
  getDivergence(wt.path),
3398
- git.getWorktreeMetadata(wt.path).catch(() => null)
3532
+ git.getWorktreeMetadata(wt.path).catch(() => null),
3533
+ params.includeSize ? calculateDirectorySize(wt.path).catch(() => null) : Promise.resolve(null)
3399
3534
  ]);
3400
3535
  return {
3401
3536
  path: resolvedPath,
3402
3537
  branch: wt.branch,
3403
3538
  isCurrent,
3404
- label: status ? deriveLabel(status, isCurrent) : "unknown",
3539
+ label: status ? deriveLabel(status, isCurrent) : isCurrent ? "current" : "unknown",
3405
3540
  status,
3406
3541
  divergence,
3407
- safeToRemove: status ? status.canRemove && !status.upstreamGone : false,
3408
- lastSyncAt: metadata?.lastSyncDate ?? null
3542
+ safeToRemove: status ? deriveSafeToRemove(status) : { safe: false, reason: "status unavailable" },
3543
+ lastSyncAt: metadata?.lastSyncDate ?? null,
3544
+ sizeBytes
3409
3545
  };
3410
3546
  })
3411
3547
  )
@@ -3414,7 +3550,7 @@ async function handleListWorktrees(ctx, params, _extra) {
3414
3550
  }
3415
3551
  async function handleGetWorktreeStatus(ctx, params, _extra) {
3416
3552
  const { git } = await getReadyService(ctx, params.repoName, {
3417
- capability: "canGetStatus",
3553
+ capability: "getStatus",
3418
3554
  toolName: "get_worktree_status"
3419
3555
  });
3420
3556
  const resolvedPath = await ensureRepoWorktreePath(ctx, params, git);
@@ -3435,7 +3571,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
3435
3571
  throw new Error(`Invalid branch name '${branchName}': ${validation.error}`);
3436
3572
  }
3437
3573
  const { service, git } = await getReadyService(ctx, params.repoName, {
3438
- capability: "canCreateWorktree",
3574
+ capability: "createWorktree",
3439
3575
  toolName: "create_worktree",
3440
3576
  ensureInitialized: true,
3441
3577
  ensureNotSyncing: true
@@ -3475,7 +3611,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
3475
3611
  }
3476
3612
  async function handleRemoveWorktree(ctx, params, _extra) {
3477
3613
  const { git } = await getReadyService(ctx, params.repoName, {
3478
- capability: "canRemoveWorktree",
3614
+ capability: "removeWorktree",
3479
3615
  toolName: "remove_worktree",
3480
3616
  ensureInitialized: true,
3481
3617
  ensureNotSyncing: true
@@ -3496,7 +3632,7 @@ async function handleRemoveWorktree(ctx, params, _extra) {
3496
3632
  }
3497
3633
  async function handleSync(ctx, params, extra) {
3498
3634
  const { service } = await getReadyService(ctx, params.repoName, {
3499
- capability: "canSync",
3635
+ capability: "sync",
3500
3636
  toolName: "sync",
3501
3637
  ensureInitialized: true
3502
3638
  });
@@ -3516,7 +3652,7 @@ async function handleSync(ctx, params, extra) {
3516
3652
  }
3517
3653
  async function handleUpdateWorktree(ctx, params, _extra) {
3518
3654
  const { git } = await getReadyService(ctx, params.repoName, {
3519
- capability: "canUpdateWorktree",
3655
+ capability: "updateWorktree",
3520
3656
  toolName: "update_worktree",
3521
3657
  ensureInitialized: true,
3522
3658
  ensureNotSyncing: true
@@ -3531,7 +3667,7 @@ async function handleUpdateWorktree(ctx, params, _extra) {
3531
3667
  }
3532
3668
  async function handleInitialize(ctx, params, extra) {
3533
3669
  const { service } = await getReadyService(ctx, params.repoName, {
3534
- capability: "canInitialize",
3670
+ capability: "initialize",
3535
3671
  toolName: "initialize",
3536
3672
  ensureNotSyncing: true
3537
3673
  });
@@ -3593,7 +3729,7 @@ function attachProgressReporter(service, extra) {
3593
3729
  // src/mcp/server.ts
3594
3730
  var REPO_NAME_DESCRIBE = "Repository name from loaded config. If omitted, uses the current repository set via set_current_repository or the only loaded repo.";
3595
3731
  var PATH_DESCRIBE_SUFFIX = "Absolute path preferred; relative paths resolve from the server's CWD.";
3596
- var SERVER_INSTRUCTIONS = "Before running git worktree operations, call `detect_context` to learn the current repo, current branch, and which capabilities are available. It reports whether the working directory is inside a sync-worktrees-managed workspace, lists sibling worktrees, and explains why any capability is disabled.";
3732
+ var SERVER_INSTRUCTIONS = "Before running git worktree operations, call `detect_context` to learn the current repo, current branch, sibling repositories under the workspace root, and which capabilities are available. It walks up to auto-discover sync-worktrees.config.{js,mjs,cjs,ts}, lists sibling worktrees, and reports per-capability {available, reason} so you can tell which tool is gated and why.";
3597
3733
  function createServer(context) {
3598
3734
  const server = new McpServer(
3599
3735
  {
@@ -3609,7 +3745,7 @@ function createServer(context) {
3609
3745
  "sync-worktrees://workspace",
3610
3746
  {
3611
3747
  title: "Workspace context",
3612
- description: "Current sync-worktrees workspace context: whether CWD is inside a managed worktree, the current branch, sibling worktrees, and available capabilities. Returns { isWorktree: false } when CWD is outside any workspace.",
3748
+ description: "Current sync-worktrees workspace context: whether CWD is inside a managed worktree, the current branch, sibling worktrees, sibling repositories, auto-discovered configPath, and per-capability {available, reason}. Returns { isWorktree: false } when CWD is outside any workspace.",
3613
3749
  mimeType: "application/json"
3614
3750
  },
3615
3751
  async (uri) => {
@@ -3633,9 +3769,12 @@ function createServer(context) {
3633
3769
  server.registerTool(
3634
3770
  "detect_context",
3635
3771
  {
3636
- description: "Detect sync-worktrees structure from a filesystem path. Reads .git file, resolves bare repo, discovers sibling worktrees. Defaults to CWD. Use when: bootstrapping from an unknown checkout with no config loaded. Returns: discovered repo root, bare repo path, all sibling worktrees, current worktree path, capabilities.",
3772
+ description: "Detect sync-worktrees structure from a filesystem path. Reads .git file, resolves bare repo, discovers sibling worktrees, walks up for a sync-worktrees.config.{js,mjs,cjs,ts}, and lists sibling bare repos under the workspace root. Defaults to CWD. Use when: bootstrapping from an unknown checkout. Returns: discovered repo root, bare repo path, all sibling worktrees, sibling repositories, current worktree path, configPath (auto-found), per-capability {available, reason}, notes[].",
3637
3773
  inputSchema: {
3638
- path: z.string().optional().describe("Directory path to inspect. Defaults to the server's CWD.")
3774
+ path: z.string().optional().describe("Directory path to inspect. Defaults to the server's CWD."),
3775
+ includeStatus: z.boolean().optional().describe(
3776
+ "If true, enriches each entry in allWorktrees with label, divergence, and staleHint. Adds one git status + rev-list per worktree. Default: false (cheap path)."
3777
+ )
3639
3778
  },
3640
3779
  annotations: {
3641
3780
  title: "Detect sync-worktrees context",
@@ -3649,9 +3788,12 @@ function createServer(context) {
3649
3788
  server.registerTool(
3650
3789
  "list_worktrees",
3651
3790
  {
3652
- description: "List all worktrees of a repository with enriched status. Returns: array of { path, branch, isCurrent, label (clean|dirty|stale|current|unknown), status, divergence (ahead/behind), safeToRemove, lastSyncAt }.",
3791
+ description: "List all worktrees of a repository with enriched status. Returns: array of { path, branch, isCurrent, label (clean|dirty|stale|current|unknown), status, divergence (ahead/behind), safeToRemove: { safe, reason }, lastSyncAt, sizeBytes }.",
3653
3792
  inputSchema: {
3654
- repoName: z.string().optional().describe(REPO_NAME_DESCRIBE)
3793
+ repoName: z.string().optional().describe(REPO_NAME_DESCRIBE),
3794
+ includeSize: z.boolean().optional().describe(
3795
+ "If true, computes the on-disk size of each worktree (in bytes). Slow on large worktrees. Default: false (sizeBytes returned as null)."
3796
+ )
3655
3797
  },
3656
3798
  annotations: {
3657
3799
  title: "List worktrees with status",