@tritard/waterbrother 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Waterbrother: Grok-powered coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/path-utils.js CHANGED
@@ -8,6 +8,11 @@ const COMMON_HOME_ALIASES = new Map([
8
8
  ["/documents", path.join(os.homedir(), "Documents")]
9
9
  ]);
10
10
 
11
+ const COMMON_HOME_PATHS = ["Desktop", "Downloads", "Documents"].map((name) => ({
12
+ lower: path.join(os.homedir(), name).toLowerCase(),
13
+ actual: path.join(os.homedir(), name)
14
+ }));
15
+
11
16
  const COMMON_READ_ONLY_HOME_ROOTS = ["Desktop", "Downloads", "Documents"].map((name) => path.join(os.homedir(), name));
12
17
 
13
18
  function isWithinPath(rootPath, targetPath) {
@@ -35,6 +40,10 @@ function canonicalizePath(targetPath) {
35
40
  return path.join(canonicalParent, path.basename(resolved));
36
41
  }
37
42
 
43
+ export function canonicalizeLoosePath(targetPath) {
44
+ return canonicalizePath(expandHomePath(targetPath || "."));
45
+ }
46
+
38
47
  function isAllowedReadOnlyHomePath(targetPath) {
39
48
  return COMMON_READ_ONLY_HOME_ROOTS.some((rootPath) => isWithinPath(rootPath, targetPath));
40
49
  }
@@ -58,15 +67,20 @@ export function expandHomePath(inputPath) {
58
67
  return path.join(replacement, raw.slice(prefix.length + 1));
59
68
  }
60
69
  }
70
+ for (const { lower, actual } of COMMON_HOME_PATHS) {
71
+ if (lowered === lower) return actual;
72
+ if (lowered.startsWith(`${lower}/`)) {
73
+ return path.join(actual, raw.slice(lower.length + 1));
74
+ }
75
+ }
61
76
  if (raw === "~") return os.homedir();
62
77
  if (raw.startsWith("~/")) return path.join(os.homedir(), raw.slice(2));
63
78
  return raw;
64
79
  }
65
80
 
66
81
  export function resolveSandboxPath(baseDir, targetPath, allowOutsideCwd = false, options = {}) {
67
- const expanded = expandHomePath(targetPath || ".");
68
82
  const baseReal = canonicalizePath(baseDir);
69
- const resolved = canonicalizePath(path.resolve(baseReal, expanded));
83
+ const resolved = canonicalizePath(path.resolve(baseReal, expandHomePath(targetPath || ".")));
70
84
  if (!allowOutsideCwd) {
71
85
  const rel = path.relative(baseReal, resolved);
72
86
  const outside = rel.startsWith("..") || path.isAbsolute(rel);
package/src/tools.js CHANGED
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
5
5
  import { exec } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
  import { normalizeAutonomyMode, normalizeExperienceMode } from "./modes.js";
8
- import { expandHomePath, resolveSandboxPath } from "./path-utils.js";
8
+ import { canonicalizeLoosePath, expandHomePath, resolveSandboxPath } from "./path-utils.js";
9
9
  import { promptKeyChoice, promptLine } from "./prompt.js";
10
10
  import { McpManager } from "./mcp.js";
11
11
 
@@ -416,7 +416,11 @@ function normalizePathPattern(value) {
416
416
  const raw = String(value || "").trim();
417
417
  if (!raw) return "";
418
418
  const expanded = expandHomePath(raw);
419
- return String(expanded).replace(/^\.\//, "").replace(/\\/g, "/");
419
+ const text = String(expanded);
420
+ const normalized = (text.startsWith("/") || raw.startsWith("~"))
421
+ ? canonicalizeLoosePath(text)
422
+ : text;
423
+ return String(normalized).replace(/^\.\//, "").replace(/\\/g, "/");
420
424
  }
421
425
 
422
426
  function looksLikeMutatingShellCommand(command) {
@@ -1036,7 +1040,11 @@ export function createToolRuntime({
1036
1040
  function contractAllows(toolName, args = {}) {
1037
1041
  if (!currentTurn.contract) return { ok: !toolRequiresContract(toolName, args), reason: "No active contract" };
1038
1042
  const touchedPaths = getTouchedPathsForTool(toolName, args);
1039
- if (touchedPaths.length > 0 && !touchedPaths.every((item) => matchesAnyPattern(item, currentTurn.contract.paths))) {
1043
+ const allowedPaths = normalizePathList([
1044
+ ...(Array.isArray(currentTurn.contract.paths) ? currentTurn.contract.paths : []),
1045
+ ...getCurrentWriteRoots()
1046
+ ]);
1047
+ if (touchedPaths.length > 0 && !touchedPaths.every((item) => matchesAnyPattern(item, allowedPaths))) {
1040
1048
  return { ok: false, reason: `Action outside declared contract scope: ${touchedPaths.join(", ")}` };
1041
1049
  }
1042
1050
  if (toolName === "run_shell") {
@@ -2201,6 +2209,7 @@ ${clipped}`;
2201
2209
  function beginTurn() {
2202
2210
  checkpointCreatedThisTurn = false;
2203
2211
  currentReadOnlyRoots = [];
2212
+ currentWriteRoots = [];
2204
2213
  currentTurn = buildDefaultTurnRecord();
2205
2214
  }
2206
2215