@lousy-agents/mcp 5.15.7 → 5.16.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.
Files changed (2) hide show
  1. package/dist/mcp-server.js +473 -31
  2. package/package.json +1 -1
@@ -6559,7 +6559,7 @@ function escapeJsonPtr(str) {
6559
6559
 
6560
6560
 
6561
6561
  },
6562
- 8705(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
6562
+ 6887(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
6563
6563
  // NAMESPACE OBJECT: ../../node_modules/micromark/lib/constructs.js
6564
6564
  var constructs_namespaceObject = {};
6565
6565
  __webpack_require__.r(constructs_namespaceObject);
@@ -26479,6 +26479,8 @@ const EMPTY_COMPLETION_RESULT = {
26479
26479
  }
26480
26480
  };
26481
26481
  //# sourceMappingURL=mcp.js.map
26482
+ // EXTERNAL MODULE: external "node:fs/promises"
26483
+ var promises_ = __webpack_require__(1455);
26482
26484
  // EXTERNAL MODULE: external "node:path"
26483
26485
  var external_node_path_ = __webpack_require__(6760);
26484
26486
  ;// CONCATENATED MODULE: ../core/src/gateways/action-version-gateway.ts
@@ -26524,8 +26526,6 @@ var external_node_path_ = __webpack_require__(6760);
26524
26526
  * List of known action names for convenience
26525
26527
  */ const KNOWN_ACTIONS = Object.keys(DEFAULT_ACTION_VERSIONS);
26526
26528
 
26527
- // EXTERNAL MODULE: external "node:fs/promises"
26528
- var promises_ = __webpack_require__(1455);
26529
26529
  // EXTERNAL MODULE: external "node:fs"
26530
26530
  var external_node_fs_ = __webpack_require__(3024);
26531
26531
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/errors.js
@@ -26579,16 +26579,24 @@ function createBoundedReadStream(opened, maxBytes) {
26579
26579
  }
26580
26580
 
26581
26581
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
26582
+
26582
26583
  function isZero(value) {
26583
26584
  return value === 0 || value === 0n;
26584
26585
  }
26586
+ function sameStatValue(left, right) {
26587
+ return typeof left === typeof right ? left === right : BigInt(left) === BigInt(right);
26588
+ }
26589
+ function sha256Hex(data, encoding) {
26590
+ const buffer = typeof data === "string" ? Buffer.from(data, encoding ?? "utf8") : data;
26591
+ return (0,external_node_crypto_.createHash)("sha256").update(buffer).digest("hex");
26592
+ }
26585
26593
  function file_identity_sameFileIdentity(left, right, platform = process.platform) {
26586
- if (left.ino !== right.ino) {
26594
+ if (!sameStatValue(left.ino, right.ino)) {
26587
26595
  return false;
26588
26596
  }
26589
26597
  // On Windows, path-based stat calls can report dev=0 while fd-based stat
26590
26598
  // reports a real volume serial; treat either-side dev=0 as "unknown device".
26591
- if (left.dev === right.dev) {
26599
+ if (sameStatValue(left.dev, right.dev)) {
26592
26600
  return true;
26593
26601
  }
26594
26602
  return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
@@ -27982,6 +27990,377 @@ async function runPinnedPathHelper(params) {
27982
27990
  }
27983
27991
  }
27984
27992
 
27993
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
27994
+
27995
+
27996
+
27997
+
27998
+ const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
27999
+ function getGlobalManagers() {
28000
+ const globalWithState = globalThis;
28001
+ if (!globalWithState[GLOBAL_STATE_KEY]) {
28002
+ globalWithState[GLOBAL_STATE_KEY] = new Map();
28003
+ }
28004
+ return globalWithState[GLOBAL_STATE_KEY];
28005
+ }
28006
+ function resolveManagerState(key) {
28007
+ const managers = getGlobalManagers();
28008
+ let state = managers.get(key);
28009
+ if (!state) {
28010
+ state = { cleanupRegistered: false, held: new Map() };
28011
+ managers.set(key, state);
28012
+ }
28013
+ return state;
28014
+ }
28015
+ async function readLockSnapshot(lockPath) {
28016
+ try {
28017
+ const stat = await promises_.lstat(lockPath);
28018
+ const raw = await promises_.readFile(lockPath, "utf8");
28019
+ try {
28020
+ const parsed = JSON.parse(raw);
28021
+ const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
28022
+ ? parsed
28023
+ : null;
28024
+ return { raw, payload, stat };
28025
+ }
28026
+ catch {
28027
+ return { raw, payload: null, stat };
28028
+ }
28029
+ }
28030
+ catch (err) {
28031
+ if (err.code === "ENOENT") {
28032
+ return null;
28033
+ }
28034
+ throw err;
28035
+ }
28036
+ }
28037
+ function snapshotMatches(current, observed) {
28038
+ if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
28039
+ return false;
28040
+ }
28041
+ if (observed.raw !== undefined) {
28042
+ return current.raw === observed.raw;
28043
+ }
28044
+ return observed.stat !== undefined && current.stat !== undefined;
28045
+ }
28046
+ async function removeLockIfUnchanged(lockPath, observed) {
28047
+ const current = await readLockSnapshot(lockPath);
28048
+ if (!current || !observed) {
28049
+ return false;
28050
+ }
28051
+ if (!snapshotMatches(current, observed)) {
28052
+ // The lock changed after we decided it was stale. Leave the fresh holder's
28053
+ // file alone; deleting by path here would break mutual exclusion.
28054
+ return false;
28055
+ }
28056
+ await promises_.rm(lockPath, { force: true }).catch(() => undefined);
28057
+ return true;
28058
+ }
28059
+ async function lockSnapshotStillPresent(lockPath, observed) {
28060
+ const current = await readLockSnapshot(lockPath);
28061
+ return !!current && !!observed && snapshotMatches(current, observed);
28062
+ }
28063
+ async function removeStaleLockIfAllowed(params) {
28064
+ if (!params.shouldRemoveStaleLock) {
28065
+ return "not-approved";
28066
+ }
28067
+ if (params.snapshot.raw === undefined) {
28068
+ return "not-approved";
28069
+ }
28070
+ if (!(await params.shouldRemoveStaleLock({
28071
+ lockPath: params.lockPath,
28072
+ normalizedTargetPath: params.normalizedTargetPath,
28073
+ raw: params.snapshot.raw,
28074
+ payload: params.snapshot.payload,
28075
+ }))) {
28076
+ return "not-approved";
28077
+ }
28078
+ const current = await readLockSnapshot(params.lockPath);
28079
+ if (!current || !snapshotMatches(current, params.snapshot)) {
28080
+ return "changed";
28081
+ }
28082
+ try {
28083
+ await promises_.rm(params.lockPath, { force: true });
28084
+ }
28085
+ catch (err) {
28086
+ if (err.code === "ENOENT") {
28087
+ return "changed";
28088
+ }
28089
+ return "not-approved";
28090
+ }
28091
+ return "removed";
28092
+ }
28093
+ function snapshotMatchesSync(lockPath, observed) {
28094
+ try {
28095
+ const stat = external_node_fs_.lstatSync(lockPath);
28096
+ if (observed.stat && !file_identity_sameFileIdentity(observed.stat, stat)) {
28097
+ return false;
28098
+ }
28099
+ return observed.raw === undefined || external_node_fs_.readFileSync(lockPath, "utf8") === observed.raw;
28100
+ }
28101
+ catch {
28102
+ return false;
28103
+ }
28104
+ }
28105
+ async function resolveNormalizedTargetPath(targetPath) {
28106
+ const resolved = external_node_path_.resolve(targetPath);
28107
+ const dir = external_node_path_.dirname(resolved);
28108
+ await promises_.mkdir(dir, { recursive: true });
28109
+ try {
28110
+ return external_node_path_.join(await promises_.realpath(dir), external_node_path_.basename(resolved));
28111
+ }
28112
+ catch {
28113
+ return resolved;
28114
+ }
28115
+ }
28116
+ function computeDelayMs(retry, attempt) {
28117
+ const minTimeout = retry.minTimeout ?? 50;
28118
+ const maxTimeout = retry.maxTimeout ?? 1000;
28119
+ const factor = retry.factor ?? 1;
28120
+ const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
28121
+ const jitter = retry.randomize ? 1 + Math.random() : 1;
28122
+ return Math.min(maxTimeout, Math.round(base * jitter));
28123
+ }
28124
+ async function defaultShouldReclaim(params) {
28125
+ const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
28126
+ const createdAtMs = Date.parse(createdAt);
28127
+ if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
28128
+ return true;
28129
+ }
28130
+ try {
28131
+ const stat = await promises_.stat(params.lockPath);
28132
+ return params.nowMs - stat.mtimeMs > params.staleMs;
28133
+ }
28134
+ catch {
28135
+ return true;
28136
+ }
28137
+ }
28138
+ function releaseAllLocksSync(state) {
28139
+ for (const [normalizedTargetPath, held] of state.held) {
28140
+ void held.handle.close().catch(() => undefined);
28141
+ try {
28142
+ if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
28143
+ external_node_fs_.rmSync(held.lockPath, { force: true });
28144
+ }
28145
+ }
28146
+ catch {
28147
+ // Best-effort process-exit cleanup.
28148
+ }
28149
+ state.held.delete(normalizedTargetPath);
28150
+ }
28151
+ }
28152
+ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
28153
+ const current = state.held.get(normalizedTargetPath);
28154
+ if (current !== held) {
28155
+ return false;
28156
+ }
28157
+ if (opts.force) {
28158
+ held.count = 0;
28159
+ }
28160
+ else {
28161
+ held.count -= 1;
28162
+ if (held.count > 0) {
28163
+ return false;
28164
+ }
28165
+ }
28166
+ if (held.releasePromise) {
28167
+ await held.releasePromise.catch(() => undefined);
28168
+ return true;
28169
+ }
28170
+ state.held.delete(normalizedTargetPath);
28171
+ held.releasePromise = (async () => {
28172
+ await held.handle.close().catch(() => undefined);
28173
+ await removeLockIfUnchanged(held.lockPath, held.snapshot);
28174
+ })();
28175
+ try {
28176
+ await held.releasePromise;
28177
+ return true;
28178
+ }
28179
+ finally {
28180
+ held.releasePromise = undefined;
28181
+ }
28182
+ }
28183
+ function createSidecarLockManager(key) {
28184
+ const state = resolveManagerState(key);
28185
+ function ensureExitCleanupRegistered() {
28186
+ if (state.cleanupRegistered) {
28187
+ return;
28188
+ }
28189
+ state.cleanupRegistered = true;
28190
+ process.on("exit", () => releaseAllLocksSync(state));
28191
+ }
28192
+ async function acquire(options) {
28193
+ ensureExitCleanupRegistered();
28194
+ const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
28195
+ const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
28196
+ const held = state.held.get(normalizedTargetPath);
28197
+ if (held && options.allowReentrant) {
28198
+ held.count += 1;
28199
+ const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
28200
+ return {
28201
+ lockPath,
28202
+ normalizedTargetPath,
28203
+ release,
28204
+ [Symbol.asyncDispose]: release,
28205
+ };
28206
+ }
28207
+ const startedAt = Date.now();
28208
+ const retry = options.retry ?? {};
28209
+ const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
28210
+ let attempt = 0;
28211
+ while (true) {
28212
+ let handle = null;
28213
+ try {
28214
+ handle = await promises_.open(lockPath, "wx");
28215
+ const payload = await options.payload();
28216
+ const raw = `${JSON.stringify(payload, null, 2)}\n`;
28217
+ await handle.writeFile(raw, "utf8");
28218
+ const snapshot = { raw, payload, stat: await handle.stat() };
28219
+ const createdHeld = {
28220
+ count: 1,
28221
+ handle,
28222
+ lockPath,
28223
+ snapshot,
28224
+ acquiredAt: Date.now(),
28225
+ metadata: options.metadata ?? {},
28226
+ };
28227
+ state.held.set(normalizedTargetPath, createdHeld);
28228
+ const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
28229
+ return {
28230
+ lockPath,
28231
+ normalizedTargetPath,
28232
+ release,
28233
+ [Symbol.asyncDispose]: release,
28234
+ };
28235
+ }
28236
+ catch (err) {
28237
+ if (handle) {
28238
+ const failedSnapshot = { payload: null };
28239
+ try {
28240
+ failedSnapshot.stat = await handle.stat();
28241
+ }
28242
+ catch {
28243
+ // Best-effort cleanup of a failed exclusive create.
28244
+ }
28245
+ const current = state.held.get(normalizedTargetPath);
28246
+ if (current?.handle === handle) {
28247
+ state.held.delete(normalizedTargetPath);
28248
+ }
28249
+ // If payload serialization/write fails, the file may be empty or
28250
+ // partial JSON, so remove while our exclusive handle is still open.
28251
+ await promises_.rm(lockPath, { force: true }).catch(() => undefined);
28252
+ await handle.close().catch(() => undefined);
28253
+ // Windows can refuse removing an open file; retry after close but
28254
+ // only if the path still points at the file identity we created.
28255
+ await removeLockIfUnchanged(lockPath, failedSnapshot);
28256
+ }
28257
+ if (err.code !== "EEXIST") {
28258
+ throw err;
28259
+ }
28260
+ const nowMs = Date.now();
28261
+ const snapshot = await readLockSnapshot(lockPath);
28262
+ if (!snapshot) {
28263
+ continue;
28264
+ }
28265
+ const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
28266
+ if (await shouldReclaim({
28267
+ lockPath,
28268
+ normalizedTargetPath,
28269
+ payload: snapshot?.payload ?? null,
28270
+ staleMs: options.staleMs,
28271
+ nowMs,
28272
+ heldByThisProcess: state.held.has(normalizedTargetPath),
28273
+ })) {
28274
+ if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
28275
+ continue;
28276
+ }
28277
+ const staleRecovery = options.staleRecovery ?? "fail-closed";
28278
+ if (staleRecovery === "remove-if-unchanged") {
28279
+ const removal = await removeStaleLockIfAllowed({
28280
+ lockPath,
28281
+ normalizedTargetPath,
28282
+ snapshot,
28283
+ shouldRemoveStaleLock: options.shouldRemoveStaleLock,
28284
+ });
28285
+ if (removal === "removed" || removal === "changed") {
28286
+ continue;
28287
+ }
28288
+ }
28289
+ throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
28290
+ code: "file_lock_stale",
28291
+ lockPath,
28292
+ normalizedTargetPath,
28293
+ });
28294
+ }
28295
+ const elapsed = Date.now() - startedAt;
28296
+ if ((options.timeoutMs !== undefined &&
28297
+ options.timeoutMs !== Number.POSITIVE_INFINITY &&
28298
+ elapsed >= options.timeoutMs) ||
28299
+ (maxRetries !== undefined && attempt >= maxRetries)) {
28300
+ throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
28301
+ code: "file_lock_timeout",
28302
+ lockPath,
28303
+ normalizedTargetPath,
28304
+ });
28305
+ }
28306
+ const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
28307
+ ? Number.POSITIVE_INFINITY
28308
+ : Math.max(0, options.timeoutMs - elapsed);
28309
+ const delay = Math.min(computeDelayMs(retry, attempt), remaining);
28310
+ attempt += 1;
28311
+ await new Promise((resolve) => setTimeout(resolve, delay));
28312
+ }
28313
+ }
28314
+ }
28315
+ async function withLock(options, fn) {
28316
+ const lock = await acquire(options);
28317
+ try {
28318
+ return await fn();
28319
+ }
28320
+ finally {
28321
+ await lock.release();
28322
+ }
28323
+ }
28324
+ async function drain() {
28325
+ for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
28326
+ await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
28327
+ }
28328
+ }
28329
+ function reset() {
28330
+ releaseAllLocksSync(state);
28331
+ }
28332
+ function heldEntries() {
28333
+ return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
28334
+ normalizedTargetPath,
28335
+ lockPath: held.lockPath,
28336
+ acquiredAt: held.acquiredAt,
28337
+ metadata: held.metadata,
28338
+ forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
28339
+ }));
28340
+ }
28341
+ return { acquire, withLock, drain, reset, heldEntries };
28342
+ }
28343
+ async function withSidecarLock(targetPath, options, fn) {
28344
+ const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
28345
+ const { managerKey: _managerKey, ...acquireOptions } = options;
28346
+ return await manager.withLock({ ...acquireOptions, targetPath }, fn);
28347
+ }
28348
+
28349
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
28350
+ let test_hooks_fsSafeTestHooks;
28351
+ function allowFsSafeTestHooks() {
28352
+ return false || process.env.VITEST === "true";
28353
+ }
28354
+ function getFsSafeTestHooks() {
28355
+ return test_hooks_fsSafeTestHooks;
28356
+ }
28357
+ function __setFsSafeTestHooksForTest(hooks) {
28358
+ if (hooks && !allowFsSafeTestHooks()) {
28359
+ throw new Error("__setFsSafeTestHooksForTest is only available in tests");
28360
+ }
28361
+ test_hooks_fsSafeTestHooks = hooks;
28362
+ }
28363
+
27985
28364
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
27986
28365
 
27987
28366
 
@@ -27995,6 +28374,8 @@ async function runPinnedPathHelper(params) {
27995
28374
 
27996
28375
 
27997
28376
 
28377
+
28378
+
27998
28379
  function pinned_write_byteLength(input, encoding) {
27999
28380
  return typeof input === "string"
28000
28381
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -28052,6 +28433,12 @@ async function runPinnedWriteHelper(params) {
28052
28433
  validatePinnedOperationPayload({
28053
28434
  relativeParentPath: params.relativeParentPath,
28054
28435
  });
28436
+ // The Python helper deliberately enforces the strict post-rename inode
28437
+ // contract. The explicit compatibility policy therefore uses the guarded
28438
+ // Node fallback, where content verification can replace that one check.
28439
+ if (params.onRenameIdentityMismatch === "verify-content") {
28440
+ return await runPinnedWriteFallback(params);
28441
+ }
28055
28442
  if (getFsSafePythonConfig().mode === "off") {
28056
28443
  return await runPinnedWriteFallback(params);
28057
28444
  }
@@ -28066,8 +28453,11 @@ async function runPinnedWriteHelper(params) {
28066
28453
  throw error;
28067
28454
  }
28068
28455
  }
28456
+ const input = params.input.kind === "stream"
28457
+ ? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
28458
+ : params.input;
28069
28459
  const payload = {
28070
- base64: await inputToBase64(params.input, params.maxBytes),
28460
+ base64: await inputToBase64(input, params.maxBytes),
28071
28461
  basename: params.basename,
28072
28462
  maxBytes: params.maxBytes ?? -1,
28073
28463
  mkdir: params.mkdir,
@@ -28085,11 +28475,32 @@ async function runPinnedWriteHelper(params) {
28085
28475
  }
28086
28476
  catch (error) {
28087
28477
  if (canFallbackFromPythonError(error)) {
28088
- return await runPinnedWriteFallback(params);
28478
+ return await runPinnedWriteFallback({ ...params, input });
28089
28479
  }
28090
28480
  throw error;
28091
28481
  }
28092
28482
  }
28483
+ async function runPinnedWriteWithRenamePolicy(params) {
28484
+ const { targetPath, renameIdentity, ...writeParams } = params;
28485
+ if (renameIdentity !== "verify-content-with-lock") {
28486
+ return await runPinnedWriteHelper(writeParams);
28487
+ }
28488
+ const relativeTargetPath = writeParams.relativeParentPath
28489
+ ? `${writeParams.relativeParentPath}/${writeParams.basename}`
28490
+ : writeParams.basename;
28491
+ const lockPath = external_node_path_.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
28492
+ return await withSidecarLock(writeParams.rootPath, {
28493
+ managerKey: `fs-safe.write:${targetPath}`,
28494
+ lockPath,
28495
+ staleMs: 30_000,
28496
+ timeoutMs: 5_000,
28497
+ payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
28498
+ retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
28499
+ }, async () => await runPinnedWriteHelper({
28500
+ ...writeParams,
28501
+ onRenameIdentityMismatch: "verify-content",
28502
+ }));
28503
+ }
28093
28504
  async function runPinnedCopyHelper(params) {
28094
28505
  assertSafeBasename(params.basename);
28095
28506
  validatePinnedOperationPayload({
@@ -28145,7 +28556,10 @@ async function runPinnedWriteFallback(params) {
28145
28556
  else {
28146
28557
  await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
28147
28558
  }
28559
+ await handle.sync();
28148
28560
  const stat = await handle.stat();
28561
+ await handle.close().catch(() => undefined);
28562
+ await syncDirectoryBestEffort(parentPath);
28149
28563
  created = false;
28150
28564
  return { dev: stat.dev, ino: stat.ino };
28151
28565
  }
@@ -28193,11 +28607,49 @@ async function runPinnedWriteFallback(params) {
28193
28607
  await withAsyncDirectoryGuards([parentGuard], async () => {
28194
28608
  await promises_.rename(tempPath, targetPath);
28195
28609
  renamed = true;
28610
+ await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
28196
28611
  await syncDirectoryBestEffort(parentPath);
28197
28612
  targetStat = await promises_.lstat(targetPath);
28198
- if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
28613
+ if (targetStat.isSymbolicLink()) {
28199
28614
  throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
28200
28615
  }
28616
+ if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
28617
+ // On filesystems like rclone FUSE, rename(2) can give the destination a
28618
+ // different inode from the source temp fd even with zero concurrency. The
28619
+ // caller must ensure mutual exclusion before passing "verify-content";
28620
+ // fall back to a content hash for this rename-boundary check only.
28621
+ if (params.onRenameIdentityMismatch !== "verify-content") {
28622
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
28623
+ }
28624
+ if (params.input.kind !== "buffer") {
28625
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
28626
+ }
28627
+ const expectedHash = sha256Hex(params.input.data, params.input.encoding);
28628
+ const readFlags = external_node_fs_.constants.O_RDONLY |
28629
+ (process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_.constants
28630
+ ? external_node_fs_.constants.O_NOFOLLOW
28631
+ : 0);
28632
+ const readHandle = await promises_.open(targetPath, readFlags);
28633
+ let actualHash;
28634
+ let readHandleStat;
28635
+ try {
28636
+ // Capture fd-based identity before reading — this is stable across all
28637
+ // subsequent lookups (on FUSE and locally), unlike the lstat-based
28638
+ // targetStat that triggered this fallback.
28639
+ readHandleStat = await readHandle.stat();
28640
+ actualHash = sha256Hex(await readHandle.readFile());
28641
+ }
28642
+ finally {
28643
+ await readHandle.close().catch(() => undefined);
28644
+ }
28645
+ if (actualHash !== expectedHash) {
28646
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
28647
+ }
28648
+ // Replace the unreliable lstat-based targetStat with the fd-based stat so
28649
+ // the returned identity is consistent with what subsequent verifications
28650
+ // (e.g. verifyAtomicWriteResult) will obtain by opening the same file.
28651
+ targetStat = readHandleStat;
28652
+ }
28201
28653
  });
28202
28654
  }
28203
28655
  catch (error) {
@@ -29293,21 +29745,6 @@ function normalizePinnedPathError(error) {
29293
29745
  });
29294
29746
  }
29295
29747
 
29296
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
29297
- let test_hooks_fsSafeTestHooks;
29298
- function allowFsSafeTestHooks() {
29299
- return false || process.env.VITEST === "true";
29300
- }
29301
- function getFsSafeTestHooks() {
29302
- return test_hooks_fsSafeTestHooks;
29303
- }
29304
- function __setFsSafeTestHooksForTest(hooks) {
29305
- if (hooks && !allowFsSafeTestHooks()) {
29306
- throw new Error("__setFsSafeTestHooksForTest is only available in tests");
29307
- }
29308
- test_hooks_fsSafeTestHooks = hooks;
29309
- }
29310
-
29311
29748
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
29312
29749
  function stringifyJsonDocument(value, replacer, space) {
29313
29750
  const text = JSON.stringify(value, replacer, space);
@@ -29643,6 +30080,7 @@ class RootHandle {
29643
30080
  data,
29644
30081
  mkdir: this.defaults.mkdir,
29645
30082
  mode: this.defaults.mode,
30083
+ renameIdentity: this.defaults.renameIdentity,
29646
30084
  ...options,
29647
30085
  denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
29648
30086
  });
@@ -30077,21 +30515,23 @@ async function writeFileInRoot(root, params) {
30077
30515
  async function commitPinnedWriteInRoot(root, pinned, params) {
30078
30516
  let identity;
30079
30517
  try {
30080
- identity = await runPinnedWriteHelper({
30518
+ identity = await runPinnedWriteWithRenamePolicy({
30081
30519
  rootPath: pinned.rootReal,
30082
30520
  relativeParentPath: pinned.relativeParentPath,
30083
30521
  basename: pinned.basename,
30522
+ targetPath: pinned.targetPath,
30523
+ renameIdentity: params.renameIdentity,
30084
30524
  mkdir: params.mkdir !== false,
30085
30525
  mode: params.mode ?? pinned.mode,
30086
30526
  overwrite: params.overwrite,
30087
- input: {
30088
- kind: "buffer",
30089
- data: params.data,
30090
- encoding: params.encoding,
30091
- },
30527
+ input: { kind: "buffer", data: params.data, encoding: params.encoding },
30092
30528
  });
30093
30529
  }
30094
30530
  catch (error) {
30531
+ const errorCode = error?.code;
30532
+ if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
30533
+ throw error;
30534
+ }
30095
30535
  if (params.overwrite === false && isAlreadyExistsError(error)) {
30096
30536
  throw new errors_FsSafeError("already-exists", "file already exists", {
30097
30537
  cause: error instanceof Error ? error : undefined,
@@ -37113,6 +37553,7 @@ function createWorkflowGateway(cwd) {
37113
37553
 
37114
37554
 
37115
37555
 
37556
+
37116
37557
  /**
37117
37558
  * Parses a workflow file and extracts action references.
37118
37559
  */ async function parseWorkflowFile(rootDir, relativePath) {
@@ -37143,7 +37584,8 @@ function createWorkflowGateway(cwd) {
37143
37584
  * Analyzes GitHub Action versions used across all workflow files.
37144
37585
  */ const analyzeActionVersionsHandler = async (args)=>{
37145
37586
  const dir = args.targetDir || process.cwd();
37146
- if (!await file_system_utils_fileExists(dir)) {
37587
+ const dirStat = await (0,promises_.stat)(dir).catch(()=>null);
37588
+ if (dirStat === null || !dirStat.isDirectory()) {
37147
37589
  return types_errorResponse(`Target directory does not exist: ${dir}`);
37148
37590
  }
37149
37591
  const workflowsRelativeDir = (0,external_node_path_.join)(".github", "workflows");
@@ -68590,4 +69032,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
68590
69032
  // module factories are used so entry inlining is disabled
68591
69033
  // startup
68592
69034
  // Load entry module and return exports
68593
- var __webpack_exports__ = __webpack_require__(8705);
69035
+ var __webpack_exports__ = __webpack_require__(6887);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/mcp",
3
- "version": "5.15.7",
3
+ "version": "5.16.0",
4
4
  "description": "MCP server for lousy-agents - provides AI coding assistant tools via the Model Context Protocol",
5
5
  "type": "module",
6
6
  "repository": {