@lousy-agents/cli 5.14.8 → 5.14.10

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.
@@ -18,14 +18,14 @@
18
18
  },
19
19
  "devDependencies": {
20
20
  "@biomejs/biome": "2.4.15",
21
- "@lousy-agents/mcp": "5.14.7",
21
+ "@lousy-agents/mcp": "5.14.8",
22
22
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
23
23
  "@testcontainers/postgresql": "11.14.0",
24
24
  "@types/node": "24.12.4",
25
25
  "@upstash/context7-mcp": "2.2.5",
26
26
  "chance": "1.1.13",
27
27
  "testcontainers": "11.14.0",
28
- "tsx": "4.22.0",
28
+ "tsx": "4.22.1",
29
29
  "typescript": "6.0.3",
30
30
  "vitest": "4.1.6"
31
31
  }
@@ -956,9 +956,9 @@
956
956
  }
957
957
  },
958
958
  "node_modules/@lousy-agents/mcp": {
959
- "version": "5.14.7",
960
- "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.14.7.tgz",
961
- "integrity": "sha512-+M23eh9ZSFwsEOaoPVp5OKOdi/R9Yk+5h0v/SyxFxf8KT7ID0uhaoCuhmM5LyAG5WteFFqhEXx+W+zRp6tTRAw==",
959
+ "version": "5.14.8",
960
+ "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.14.8.tgz",
961
+ "integrity": "sha512-stW+xTi0Y/fEs/N8oVJ6u3RUi4Xd3dLG8NMdsB7tcP1KIYTQKE/CXHbU9QElLRSuFZwNFPtf15G4Pm4QTes1Pg==",
962
962
  "dev": true,
963
963
  "license": "BSD-2-Clause-Patent",
964
964
  "dependencies": {
@@ -5423,9 +5423,9 @@
5423
5423
  "optional": true
5424
5424
  },
5425
5425
  "node_modules/tsx": {
5426
- "version": "4.22.0",
5427
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
5428
- "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
5426
+ "version": "4.22.1",
5427
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz",
5428
+ "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==",
5429
5429
  "dev": true,
5430
5430
  "license": "MIT",
5431
5431
  "dependencies": {
@@ -29,14 +29,14 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@biomejs/biome": "2.4.15",
32
- "@lousy-agents/mcp": "5.14.7",
32
+ "@lousy-agents/mcp": "5.14.8",
33
33
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
34
34
  "@testcontainers/postgresql": "11.14.0",
35
35
  "@types/node": "24.12.4",
36
36
  "@upstash/context7-mcp": "2.2.5",
37
37
  "chance": "1.1.13",
38
38
  "testcontainers": "11.14.0",
39
- "tsx": "4.22.0",
39
+ "tsx": "4.22.1",
40
40
  "typescript": "6.0.3",
41
41
  "vitest": "4.1.6"
42
42
  }
@@ -20,12 +20,12 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@biomejs/biome": "2.4.15",
23
- "@lousy-agents/mcp": "5.14.7",
23
+ "@lousy-agents/mcp": "5.14.8",
24
24
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
25
25
  "@types/node": "24.12.4",
26
26
  "@upstash/context7-mcp": "2.2.5",
27
27
  "chance": "1.1.13",
28
- "tsx": "4.22.0",
28
+ "tsx": "4.22.1",
29
29
  "typescript": "6.0.3",
30
30
  "vitest": "4.1.6"
31
31
  }
package/dist/index.js CHANGED
@@ -2990,7 +2990,7 @@ exports.basename = (path, { windows } = {}) => {
2990
2990
 
2991
2991
 
2992
2992
  },
2993
- 9119(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
2993
+ 1809(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
2994
2994
  // NAMESPACE OBJECT: ../../node_modules/micromark/lib/constructs.js
2995
2995
  var constructs_namespaceObject = {};
2996
2996
  __webpack_require__.r(constructs_namespaceObject);
@@ -3210,6 +3210,9 @@ function path_isPathInside(root, target) {
3210
3210
  const firstSegment = relative.split(external_node_path_.posix.sep)[0];
3211
3211
  return relative === "" || (firstSegment !== ".." && !external_node_path_.isAbsolute(relative));
3212
3212
  }
3213
+ function isPathRelativeEscape(relativePath) {
3214
+ return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
3215
+ }
3213
3216
  function resolveSafeBaseDir(rootDir) {
3214
3217
  const resolved = path.resolve(rootDir);
3215
3218
  return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
@@ -3275,7 +3278,7 @@ function splitSafeRelativePath(relativePath) {
3275
3278
  function resolveSafeRelativePath(rootDir, relativePath) {
3276
3279
  const root = path.resolve(rootDir);
3277
3280
  const target = path.resolve(root, ...splitSafeRelativePath(relativePath));
3278
- if (target !== root && !target.startsWith(root + path.sep)) {
3281
+ if (!path_isPathInside(root, target)) {
3279
3282
  throw new FsSafeError("outside-workspace", "relative path escapes root");
3280
3283
  }
3281
3284
  return target;
@@ -3358,17 +3361,17 @@ function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targe
3358
3361
 
3359
3362
 
3360
3363
 
3364
+
3361
3365
  function isSameOrChildPath(candidate, parent) {
3362
- return candidate === parent || candidate.startsWith(`${parent}${external_node_path_.sep}`);
3363
- }
3364
- function isPathEscape(relativePath) {
3365
- return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
3366
+ const parentPrefix = parent.endsWith(external_node_path_.sep) ? parent : `${parent}${external_node_path_.sep}`;
3367
+ return candidate === parent || candidate.startsWith(parentPrefix);
3366
3368
  }
3367
3369
  async function mkdirPathComponentsWithGuards(params) {
3368
3370
  const root = external_node_path_.resolve(params.rootReal);
3371
+ const rootCanonical = external_node_path_.resolve(await promises_.realpath(root));
3369
3372
  const target = external_node_path_.resolve(params.targetPath);
3370
3373
  const relative = external_node_path_.relative(root, target);
3371
- if (isPathEscape(relative)) {
3374
+ if (isPathRelativeEscape(relative)) {
3372
3375
  throw new errors_FsSafeError("outside-workspace", "directory is outside workspace root");
3373
3376
  }
3374
3377
  let current = root;
@@ -3391,7 +3394,7 @@ async function mkdirPathComponentsWithGuards(params) {
3391
3394
  }
3392
3395
  // Node's recursive mkdir follows symlinks in missing components. Build one
3393
3396
  // segment at a time and realpath-check each segment before descending.
3394
- if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), root)) {
3397
+ if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), rootCanonical)) {
3395
3398
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
3396
3399
  }
3397
3400
  await directory_guard_createAsyncDirectoryGuard(next);
@@ -3526,26 +3529,20 @@ var external_node_child_process_ = __webpack_require__(1421);
3526
3529
 
3527
3530
 
3528
3531
  const PINNED_PYTHON_WORKER_SOURCE = String.raw `
3529
- import base64
3530
- import errno
3531
- import json
3532
- import os
3533
- import secrets
3534
- import stat
3535
- import sys
3536
-
3532
+ import base64, errno, json, os, secrets, stat, sys
3537
3533
  DIR_FLAGS = os.O_RDONLY
3538
3534
  if hasattr(os, "O_DIRECTORY"):
3539
3535
  DIR_FLAGS |= os.O_DIRECTORY
3540
3536
  if hasattr(os, "O_NOFOLLOW"):
3541
3537
  DIR_FLAGS |= os.O_NOFOLLOW
3542
3538
  READ_FLAGS = os.O_RDONLY
3539
+ if hasattr(os, "O_NONBLOCK"):
3540
+ READ_FLAGS |= os.O_NONBLOCK
3543
3541
  if hasattr(os, "O_NOFOLLOW"):
3544
3542
  READ_FLAGS |= os.O_NOFOLLOW
3545
3543
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
3546
3544
  if hasattr(os, "O_NOFOLLOW"):
3547
3545
  WRITE_FLAGS |= os.O_NOFOLLOW
3548
-
3549
3546
  def split_relative(value):
3550
3547
  if value in ("", "."):
3551
3548
  return []
@@ -3558,10 +3555,8 @@ def split_relative(value):
3558
3555
  if part == "..":
3559
3556
  raise OSError(errno.EPERM, "path traversal is not allowed")
3560
3557
  return parts
3561
-
3562
3558
  def open_dir(path_value, dir_fd=None):
3563
3559
  return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
3564
-
3565
3560
  def walk_dir(root_fd, segments, mkdir_enabled=False):
3566
3561
  current_fd = os.dup(root_fd)
3567
3562
  try:
@@ -3579,14 +3574,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
3579
3574
  except Exception:
3580
3575
  os.close(current_fd)
3581
3576
  raise
3582
-
3583
3577
  def parent_and_basename(root_fd, relative):
3584
3578
  segments = split_relative(relative)
3585
3579
  if not segments:
3586
3580
  raise OSError(errno.EPERM, "operation requires a non-root path")
3587
3581
  parent_fd = walk_dir(root_fd, segments[:-1])
3588
3582
  return parent_fd, segments[-1]
3589
-
3590
3583
  def encode_stat(st):
3591
3584
  mode = st.st_mode
3592
3585
  return {
@@ -3602,14 +3595,108 @@ def encode_stat(st):
3602
3595
  "size": st.st_size,
3603
3596
  "uid": st.st_uid,
3604
3597
  }
3605
-
3606
3598
  def reject_unsafe_endpoint(st):
3607
3599
  mode = st.st_mode
3608
3600
  if stat.S_ISLNK(mode):
3609
3601
  raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
3610
3602
  if stat.S_ISREG(mode) and st.st_nlink > 1:
3611
3603
  raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
3612
-
3604
+ def copy_bytes(source_fd, dest_fd):
3605
+ while True:
3606
+ chunk = os.read(source_fd, 65536)
3607
+ if not chunk:
3608
+ break
3609
+ view = memoryview(chunk)
3610
+ while view:
3611
+ written = os.write(dest_fd, view)
3612
+ if written <= 0:
3613
+ raise OSError(errno.EIO, "short write")
3614
+ view = view[written:]
3615
+ def write_all(fd, data):
3616
+ view = memoryview(data)
3617
+ while view:
3618
+ written = os.write(fd, view)
3619
+ if written <= 0:
3620
+ raise OSError(errno.EIO, "short write")
3621
+ view = view[written:]
3622
+ def link_unsupported(exc):
3623
+ unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
3624
+ return getattr(exc, "errno", None) in unsupported
3625
+ def link_no_replace(name, new_name, source_fd, target_fd):
3626
+ linked = False
3627
+ try:
3628
+ os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
3629
+ linked = True
3630
+ os.unlink(name, dir_fd=source_fd)
3631
+ except Exception:
3632
+ if linked:
3633
+ try: os.unlink(new_name, dir_fd=target_fd)
3634
+ except FileNotFoundError: pass
3635
+ raise
3636
+ os.fsync(source_fd)
3637
+ if source_fd != target_fd:
3638
+ os.fsync(target_fd)
3639
+ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
3640
+ source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
3641
+ dest_fd = None; success = False; dest_stat = None
3642
+ try:
3643
+ if expected is not None:
3644
+ source_stat = os.fstat(source_fd)
3645
+ if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
3646
+ raise RuntimeError("fs-safe-source-mismatch")
3647
+ dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
3648
+ copy_bytes(source_fd, dest_fd)
3649
+ os.fsync(dest_fd)
3650
+ dest_stat = os.fstat(dest_fd)
3651
+ success = True
3652
+ finally:
3653
+ os.close(source_fd)
3654
+ if dest_fd is not None:
3655
+ os.close(dest_fd)
3656
+ if dest_fd is not None and not success:
3657
+ try: os.unlink(basename, dir_fd=target_parent_fd)
3658
+ except FileNotFoundError: pass
3659
+ if unlink_source:
3660
+ try:
3661
+ os.unlink(source_name, dir_fd=source_parent_fd)
3662
+ except Exception:
3663
+ try: os.unlink(basename, dir_fd=target_parent_fd)
3664
+ except FileNotFoundError: pass
3665
+ raise
3666
+ return dest_stat
3667
+ def same_identity(left, right):
3668
+ return left.st_dev == right.st_dev and left.st_ino == right.st_ino
3669
+ def verify_temp_name(parent_fd, temp_name, expected_stat):
3670
+ current_stat = os.lstat(temp_name, dir_fd=parent_fd)
3671
+ if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
3672
+ raise RuntimeError("fs-safe-temp-mismatch")
3673
+ def verify_committed_temp(parent_fd, basename, expected_stat):
3674
+ final_stat = os.lstat(basename, dir_fd=parent_fd)
3675
+ if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
3676
+ return final_stat
3677
+ try: os.unlink(basename, dir_fd=parent_fd)
3678
+ except FileNotFoundError: pass
3679
+ raise RuntimeError("fs-safe-temp-mismatch")
3680
+ def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
3681
+ verify_temp_name(parent_fd, temp_name, expected_stat)
3682
+ if overwrite:
3683
+ os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
3684
+ return verify_committed_temp(parent_fd, basename, expected_stat)
3685
+ else:
3686
+ try:
3687
+ os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
3688
+ final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
3689
+ os.unlink(temp_name, dir_fd=parent_fd)
3690
+ return final_stat
3691
+ except OSError as exc:
3692
+ if not link_unsupported(exc):
3693
+ raise
3694
+ return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
3695
+ def assert_expected_root(root_fd, payload):
3696
+ if "rootDev" in payload or "rootIno" in payload:
3697
+ root_stat = os.fstat(root_fd)
3698
+ if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
3699
+ raise RuntimeError("fs-safe-root-mismatch")
3613
3700
  def stat_path(root_fd, payload):
3614
3701
  relative = payload.get("relativePath", "")
3615
3702
  segments = split_relative(relative)
@@ -3623,7 +3710,6 @@ def stat_path(root_fd, payload):
3623
3710
  return encode_stat(st)
3624
3711
  finally:
3625
3712
  os.close(parent_fd)
3626
-
3627
3713
  def readdir_path(root_fd, payload):
3628
3714
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
3629
3715
  try:
@@ -3639,12 +3725,9 @@ def readdir_path(root_fd, payload):
3639
3725
  return entries
3640
3726
  finally:
3641
3727
  os.close(dir_fd)
3642
-
3643
3728
  def mkdirp_path(root_fd, payload):
3644
3729
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
3645
- os.close(dir_fd)
3646
- return None
3647
-
3730
+ os.close(dir_fd); return None
3648
3731
  def remove_tree(parent_fd, basename):
3649
3732
  st = os.lstat(basename, dir_fd=parent_fd)
3650
3733
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -3657,7 +3740,6 @@ def remove_tree(parent_fd, basename):
3657
3740
  os.rmdir(basename, dir_fd=parent_fd)
3658
3741
  else:
3659
3742
  os.unlink(basename, dir_fd=parent_fd)
3660
-
3661
3743
  def remove_path(root_fd, payload):
3662
3744
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
3663
3745
  try:
@@ -3684,7 +3766,18 @@ def rename_path(root_fd, payload):
3684
3766
  try:
3685
3767
  from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
3686
3768
  reject_unsafe_endpoint(from_stat)
3687
- if not payload.get("overwrite", True):
3769
+ overwrite = payload.get("overwrite", True)
3770
+ if not overwrite and stat.S_ISREG(from_stat.st_mode):
3771
+ try:
3772
+ link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
3773
+ except OSError as exc:
3774
+ if not link_unsupported(exc):
3775
+ raise
3776
+ copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
3777
+ return None
3778
+ if not overwrite and stat.S_ISDIR(from_stat.st_mode):
3779
+ raise RuntimeError("fs-safe-directory-noreplace-unsupported")
3780
+ if not overwrite:
3688
3781
  try:
3689
3782
  os.lstat(to_base, dir_fd=to_parent_fd)
3690
3783
  raise FileExistsError(errno.EEXIST, "destination exists", to_base)
@@ -3729,19 +3822,15 @@ def write_path(root_fd, payload):
3729
3822
  except FileNotFoundError:
3730
3823
  pass
3731
3824
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
3732
- view = memoryview(data)
3733
- while view:
3734
- written = os.write(temp_fd, view)
3735
- if written <= 0:
3736
- raise OSError(errno.EIO, "short write")
3737
- view = view[written:]
3825
+ os.fchmod(temp_fd, mode)
3826
+ write_all(temp_fd, data)
3738
3827
  os.fsync(temp_fd)
3828
+ temp_stat = os.fstat(temp_fd)
3739
3829
  os.close(temp_fd)
3740
3830
  temp_fd = None
3741
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
3831
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
3742
3832
  temp_name = None
3743
3833
  os.fsync(parent_fd)
3744
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
3745
3834
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
3746
3835
  finally:
3747
3836
  if temp_fd is not None:
@@ -3771,13 +3860,8 @@ def copy_path(root_fd, payload):
3771
3860
  if max_bytes >= 0 and source_stat.st_size > max_bytes:
3772
3861
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
3773
3862
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
3774
- if not overwrite:
3775
- try:
3776
- os.lstat(basename, dir_fd=parent_fd)
3777
- raise FileExistsError(errno.EEXIST, "destination exists", basename)
3778
- except FileNotFoundError:
3779
- pass
3780
3863
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
3864
+ os.fchmod(temp_fd, mode)
3781
3865
  written_bytes = 0
3782
3866
  while True:
3783
3867
  chunk = os.read(source_fd, 65536)
@@ -3793,12 +3877,12 @@ def copy_path(root_fd, payload):
3793
3877
  raise OSError(errno.EIO, "short write")
3794
3878
  view = view[written:]
3795
3879
  os.fsync(temp_fd)
3880
+ temp_stat = os.fstat(temp_fd)
3796
3881
  os.close(temp_fd)
3797
3882
  temp_fd = None
3798
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
3883
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
3799
3884
  temp_name = None
3800
3885
  os.fsync(parent_fd)
3801
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
3802
3886
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
3803
3887
  finally:
3804
3888
  os.close(source_fd)
@@ -3815,6 +3899,7 @@ def copy_path(root_fd, payload):
3815
3899
  def run_operation(operation, root_path, payload):
3816
3900
  root_fd = open_dir(root_path)
3817
3901
  try:
3902
+ assert_expected_root(root_fd, payload)
3818
3903
  if operation == "stat":
3819
3904
  return stat_path(root_fd, payload)
3820
3905
  if operation == "readdir":
@@ -3919,6 +4004,15 @@ function mapWorkerError(response) {
3919
4004
  if (message.includes("fs-safe-source-mismatch")) {
3920
4005
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
3921
4006
  }
4007
+ if (message.includes("fs-safe-temp-mismatch")) {
4008
+ return new errors_FsSafeError("path-mismatch", "temp path changed during write");
4009
+ }
4010
+ if (message.includes("fs-safe-root-mismatch")) {
4011
+ return new errors_FsSafeError("path-mismatch", "root path changed during operation");
4012
+ }
4013
+ if (message.includes("fs-safe-directory-noreplace-unsupported")) {
4014
+ return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
4015
+ }
3922
4016
  if (code === "FileNotFoundError" || errno === 2) {
3923
4017
  return new errors_FsSafeError("not-found", "file not found");
3924
4018
  }
@@ -3936,19 +4030,19 @@ function mapWorkerError(response) {
3936
4030
  }
3937
4031
  return new errors_FsSafeError("helper-failed", message);
3938
4032
  }
3939
- function rejectPending(error) {
3940
- if (!worker) {
4033
+ function rejectPending(error, targetWorker = worker) {
4034
+ if (!targetWorker || worker !== targetWorker) {
3941
4035
  return;
3942
4036
  }
3943
- setWorkerRef(worker, false);
3944
- for (const pending of worker.pending.values()) {
4037
+ setWorkerRef(targetWorker, false);
4038
+ for (const pending of targetWorker.pending.values()) {
3945
4039
  pending.reject(error);
3946
4040
  }
3947
- worker.pending.clear();
4041
+ targetWorker.pending.clear();
3948
4042
  worker = null;
3949
4043
  }
3950
- function handleWorkerLine(line) {
3951
- if (!worker || !line.trim()) {
4044
+ function handleWorkerLine(currentWorker, line) {
4045
+ if (worker !== currentWorker || !line.trim()) {
3952
4046
  return;
3953
4047
  }
3954
4048
  let decoded;
@@ -3956,11 +4050,11 @@ function handleWorkerLine(line) {
3956
4050
  decoded = JSON.parse(line);
3957
4051
  }
3958
4052
  catch {
3959
- rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
4053
+ rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
3960
4054
  return;
3961
4055
  }
3962
4056
  if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
3963
- rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"));
4057
+ rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
3964
4058
  return;
3965
4059
  }
3966
4060
  const response = decoded;
@@ -3968,13 +4062,13 @@ function handleWorkerLine(line) {
3968
4062
  if (id === undefined) {
3969
4063
  return;
3970
4064
  }
3971
- const pending = worker.pending.get(id);
4065
+ const pending = currentWorker.pending.get(id);
3972
4066
  if (!pending) {
3973
4067
  return;
3974
4068
  }
3975
- worker.pending.delete(id);
3976
- if (worker.pending.size === 0) {
3977
- setWorkerRef(worker, false);
4069
+ currentWorker.pending.delete(id);
4070
+ if (currentWorker.pending.size === 0) {
4071
+ setWorkerRef(currentWorker, false);
3978
4072
  }
3979
4073
  if (response.ok === true) {
3980
4074
  pending.resolve(response.result);
@@ -3990,28 +4084,28 @@ function getWorker() {
3990
4084
  const child = (0,external_node_child_process_.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
3991
4085
  stdio: ["pipe", "pipe", "pipe"],
3992
4086
  });
3993
- worker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
4087
+ const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
4088
+ worker = currentWorker;
3994
4089
  child.stdout.setEncoding("utf8");
3995
4090
  child.stderr.setEncoding("utf8");
3996
4091
  child.stdout.on("data", (chunk) => {
3997
- const current = worker;
3998
- if (!current) {
4092
+ if (worker !== currentWorker) {
3999
4093
  return;
4000
4094
  }
4001
- current.stdoutBuffer += chunk;
4095
+ currentWorker.stdoutBuffer += chunk;
4002
4096
  for (;;) {
4003
- const newline = current.stdoutBuffer.indexOf("\n");
4097
+ const newline = currentWorker.stdoutBuffer.indexOf("\n");
4004
4098
  if (newline < 0) {
4005
4099
  break;
4006
4100
  }
4007
- const line = current.stdoutBuffer.slice(0, newline);
4008
- current.stdoutBuffer = current.stdoutBuffer.slice(newline + 1);
4009
- handleWorkerLine(line);
4101
+ const line = currentWorker.stdoutBuffer.slice(0, newline);
4102
+ currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
4103
+ handleWorkerLine(currentWorker, line);
4010
4104
  }
4011
4105
  });
4012
4106
  child.stderr.on("data", (chunk) => {
4013
- if (worker) {
4014
- worker.stderr = `${worker.stderr}${chunk}`.slice(-4096);
4107
+ if (worker === currentWorker) {
4108
+ currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
4015
4109
  }
4016
4110
  });
4017
4111
  child.once("error", (error) => {
@@ -4020,17 +4114,17 @@ function getWorker() {
4020
4114
  : error instanceof Error
4021
4115
  ? error
4022
4116
  : new Error(String(error));
4023
- rejectPending(mapped);
4117
+ rejectPending(mapped, currentWorker);
4024
4118
  });
4025
4119
  child.once("close", (code, signal) => {
4026
- const stderr = worker?.stderr.trim();
4027
- rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
4120
+ const stderr = currentWorker.stderr.trim();
4121
+ rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
4028
4122
  });
4029
4123
  process.once("exit", () => {
4030
4124
  child.kill("SIGTERM");
4031
4125
  });
4032
- setWorkerRef(worker, false);
4033
- return worker;
4126
+ setWorkerRef(currentWorker, false);
4127
+ return currentWorker;
4034
4128
  }
4035
4129
  function setRefable(value, ref) {
4036
4130
  if (!value) {
@@ -4096,9 +4190,7 @@ function validatePinnedOperationPayload(payload) {
4096
4190
  }
4097
4191
  }
4098
4192
  function isPinnedHelperUnavailable(error) {
4099
- return error instanceof Error &&
4100
- "code" in error &&
4101
- error.code === "helper-unavailable";
4193
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
4102
4194
  }
4103
4195
  function validatePinnedRelativePath(relativePath) {
4104
4196
  if (relativePath.length === 0 || relativePath === ".") {
@@ -4196,29 +4288,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
4196
4288
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
4197
4289
  }
4198
4290
  }
4199
- function pinned_write_createMaxBytesTransform(maxBytes) {
4200
- if (maxBytes === undefined) {
4201
- return undefined;
4202
- }
4291
+ async function writeStreamToHandle(stream, handle, maxBytes) {
4203
4292
  let bytes = 0;
4204
- return new external_node_stream_.Transform({
4205
- transform(chunk, _encoding, callback) {
4206
- bytes += chunk.byteLength;
4207
- if (bytes > maxBytes) {
4208
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
4209
- return;
4293
+ for await (const chunk of stream) {
4294
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4295
+ bytes += buffer.byteLength;
4296
+ assertWithinMaxBytes(bytes, maxBytes);
4297
+ let offset = 0;
4298
+ while (offset < buffer.byteLength) {
4299
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
4300
+ if (bytesWritten <= 0) {
4301
+ throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
4210
4302
  }
4211
- callback(null, chunk);
4212
- },
4213
- });
4214
- }
4215
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
4216
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
4217
- if (limiter) {
4218
- await (0,external_node_stream_promises_.pipeline)(stream, limiter, destination);
4219
- return;
4303
+ offset += bytesWritten;
4304
+ }
4220
4305
  }
4221
- await (0,external_node_stream_promises_.pipeline)(stream, destination);
4222
4306
  }
4223
4307
  async function inputToBase64(input, maxBytes) {
4224
4308
  if (input.kind === "buffer") {
@@ -4243,7 +4327,7 @@ async function runPinnedWriteHelper(params) {
4243
4327
  relativeParentPath: params.relativeParentPath,
4244
4328
  });
4245
4329
  if (getFsSafePythonConfig().mode === "off") {
4246
- return await runPinnedWriteFallback(params);
4330
+ return await runPinnedWriteFallbackOrThrow(params);
4247
4331
  }
4248
4332
  if (params.input.kind === "stream") {
4249
4333
  try {
@@ -4251,7 +4335,7 @@ async function runPinnedWriteHelper(params) {
4251
4335
  }
4252
4336
  catch (error) {
4253
4337
  if (canFallbackFromPythonError(error)) {
4254
- return await runPinnedWriteFallback(params);
4338
+ return await runPinnedWriteFallbackOrThrow(params, error);
4255
4339
  }
4256
4340
  throw error;
4257
4341
  }
@@ -4264,6 +4348,7 @@ async function runPinnedWriteHelper(params) {
4264
4348
  mode: params.mode || 0o600,
4265
4349
  overwrite: params.overwrite !== false,
4266
4350
  relativeParentPath: params.relativeParentPath,
4351
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
4267
4352
  };
4268
4353
  try {
4269
4354
  return await runPinnedPythonOperation({
@@ -4274,7 +4359,7 @@ async function runPinnedWriteHelper(params) {
4274
4359
  }
4275
4360
  catch (error) {
4276
4361
  if (canFallbackFromPythonError(error)) {
4277
- return await runPinnedWriteFallback(params);
4362
+ return await runPinnedWriteFallbackOrThrow(params, error);
4278
4363
  }
4279
4364
  throw error;
4280
4365
  }
@@ -4294,22 +4379,29 @@ async function runPinnedCopyHelper(params) {
4294
4379
  mode: params.mode || 0o600,
4295
4380
  overwrite: params.overwrite !== false,
4296
4381
  relativeParentPath: params.relativeParentPath,
4382
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
4297
4383
  sourceDev: params.sourceIdentity.dev,
4298
4384
  sourceIno: params.sourceIdentity.ino,
4299
4385
  sourcePath: params.sourcePath,
4300
4386
  },
4301
4387
  });
4302
4388
  }
4389
+ async function runPinnedWriteFallbackOrThrow(params, cause) {
4390
+ if (process.platform !== "win32") {
4391
+ throw new errors_FsSafeError("helper-unavailable", "Python helper is required for pinned writes on this platform", { cause });
4392
+ }
4393
+ return await runPinnedWriteFallback(params);
4394
+ }
4303
4395
  async function runPinnedWriteFallback(params) {
4304
4396
  const parentPath = params.relativeParentPath
4305
4397
  ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
4306
4398
  : params.rootPath;
4307
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
4308
4399
  if (params.mkdir) {
4309
- await withAsyncDirectoryGuards([parentGuard], async () => {
4310
- await promises_.mkdir(parentPath, { recursive: true });
4311
- });
4400
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
4312
4401
  }
4402
+ const parentGuard = params.mkdir
4403
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
4404
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
4313
4405
  const targetPath = external_node_path_.join(parentPath, params.basename);
4314
4406
  if (params.overwrite === false) {
4315
4407
  let handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_.open(targetPath, external_node_fs_.constants.O_WRONLY | external_node_fs_.constants.O_CREAT | external_node_fs_.constants.O_EXCL, params.mode), {
@@ -4331,7 +4423,7 @@ async function runPinnedWriteFallback(params) {
4331
4423
  }
4332
4424
  }
4333
4425
  else {
4334
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
4426
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
4335
4427
  }
4336
4428
  const stat = await handle.stat();
4337
4429
  created = false;
@@ -4352,7 +4444,9 @@ async function runPinnedWriteFallback(params) {
4352
4444
  ? external_node_fs_.constants.O_NOFOLLOW
4353
4445
  : 0);
4354
4446
  let handle;
4355
- let handleClosedByStream = false;
4447
+ let tempStat;
4448
+ let targetStat;
4449
+ let renamed = false;
4356
4450
  try {
4357
4451
  handle = await promises_.open(tempPath, tempFlags, params.mode);
4358
4452
  if (params.input.kind === "buffer") {
@@ -4365,29 +4459,36 @@ async function runPinnedWriteFallback(params) {
4365
4459
  }
4366
4460
  }
4367
4461
  else {
4368
- const writable = handle.createWriteStream();
4369
- writable.once("close", () => {
4370
- handleClosedByStream = true;
4371
- });
4372
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
4462
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
4373
4463
  }
4374
- if (!handleClosedByStream) {
4375
- await handle.close().catch(() => undefined);
4376
- handle = undefined;
4464
+ tempStat = await handle.stat();
4465
+ const tempPathStat = await promises_.lstat(tempPath);
4466
+ if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
4467
+ throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
4377
4468
  }
4469
+ const expectedTempStat = tempStat;
4470
+ await handle.close().catch(() => undefined);
4471
+ handle = undefined;
4378
4472
  await withAsyncDirectoryGuards([parentGuard], async () => {
4379
4473
  await promises_.rename(tempPath, targetPath);
4474
+ renamed = true;
4475
+ targetStat = await promises_.lstat(targetPath);
4476
+ if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
4477
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
4478
+ }
4380
4479
  });
4381
4480
  }
4382
4481
  catch (error) {
4383
- if (handle && !handleClosedByStream) {
4384
- await handle.close().catch(() => undefined);
4482
+ await handle?.close().catch(() => undefined);
4483
+ if (!renamed) {
4484
+ await promises_.rm(tempPath, { force: true }).catch(() => undefined);
4385
4485
  }
4386
- await promises_.rm(tempPath, { force: true }).catch(() => undefined);
4387
4486
  throw error;
4388
4487
  }
4389
- const stat = await promises_.stat(targetPath);
4390
- return { dev: stat.dev, ino: stat.ino };
4488
+ if (!targetStat) {
4489
+ throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
4490
+ }
4491
+ return { dev: targetStat.dev, ino: targetStat.ino };
4391
4492
  }
4392
4493
 
4393
4494
  // EXTERNAL MODULE: external "node:os"
@@ -4926,7 +5027,7 @@ function relativeInsideRoot(rootPath, targetPath) {
4926
5027
  if (!relative || relative === ".") {
4927
5028
  return "";
4928
5029
  }
4929
- if (relative.startsWith("..") || external_node_path_.isAbsolute(relative)) {
5030
+ if (isPathRelativeEscape(relative)) {
4930
5031
  return "";
4931
5032
  }
4932
5033
  return relative;
@@ -5277,6 +5378,15 @@ function __setFsSafeTestHooksForTest(hooks) {
5277
5378
  test_hooks_fsSafeTestHooks = hooks;
5278
5379
  }
5279
5380
 
5381
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
5382
+ function stringifyJsonDocument(value, replacer, space) {
5383
+ const text = JSON.stringify(value, replacer, space);
5384
+ if (typeof text !== "string") {
5385
+ throw new TypeError("value is not representable as a JSON document");
5386
+ }
5387
+ return text;
5388
+ }
5389
+
5280
5390
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
5281
5391
 
5282
5392
  const tempCleanupEntries = new Map();
@@ -5364,6 +5474,7 @@ async function serializePathWrite(key, run) {
5364
5474
 
5365
5475
 
5366
5476
 
5477
+
5367
5478
 
5368
5479
 
5369
5480
  function logWarn(message) {
@@ -5604,12 +5715,12 @@ class RootHandle {
5604
5715
  }
5605
5716
  async writeJson(relativePath, data, options = {}) {
5606
5717
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
5607
- const json = JSON.stringify(data, replacer, space);
5718
+ const json = stringifyJsonDocument(data, replacer, space);
5608
5719
  await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
5609
5720
  }
5610
5721
  async createJson(relativePath, data, options = {}) {
5611
5722
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
5612
- const json = JSON.stringify(data, replacer, space);
5723
+ const json = stringifyJsonDocument(data, replacer, space);
5613
5724
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
5614
5725
  }
5615
5726
  async copyIn(relativePath, sourcePath, options = {}) {
@@ -6394,6 +6505,9 @@ async function movePathFallback(root, params) {
6394
6505
  if (sourceStat.isFile() && sourceStat.nlink > 1) {
6395
6506
  throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
6396
6507
  }
6508
+ if (!params.overwrite && sourceStat.isDirectory()) {
6509
+ throw new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
6510
+ }
6397
6511
  if (!params.overwrite) {
6398
6512
  try {
6399
6513
  await promises_.lstat(target.resolved);
@@ -59042,4 +59156,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
59042
59156
  // module factories are used so entry inlining is disabled
59043
59157
  // startup
59044
59158
  // Load entry module and return exports
59045
- var __webpack_exports__ = __webpack_require__(9119);
59159
+ var __webpack_exports__ = __webpack_require__(1809);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/cli",
3
- "version": "5.14.8",
3
+ "version": "5.14.10",
4
4
  "description": "CLI scaffolding tool that sets up projects with structure, instructions, and feedback loops for AI coding assistants",
5
5
  "type": "module",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "2.4.15",
25
25
  "@eslint/eslintrc": "3.3.5",
26
- "@lousy-agents/mcp": "5.14.7",
26
+ "@lousy-agents/mcp": "5.14.8",
27
27
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
28
28
  "@testing-library/jest-dom": "6.9.1",
29
29
  "@testing-library/react": "16.3.2",