@tritard/waterbrother 0.8.2 → 0.8.4

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.2",
3
+ "version": "0.8.4",
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/cli.js CHANGED
@@ -1682,6 +1682,14 @@ function extractExplicitReadOnlyRoots(line, cwd) {
1682
1682
  return [...new Set(roots)];
1683
1683
  }
1684
1684
 
1685
+ function extractExplicitWriteRoots(line, cwd) {
1686
+ const text = String(line || "").trim();
1687
+ if (!/\b(create|make|build|write|save|generate|scaffold|edit|update|rewrite|delete|remove|move|rename)\b/i.test(text)) {
1688
+ return [];
1689
+ }
1690
+ return extractExplicitReadOnlyRoots(text, cwd);
1691
+ }
1692
+
1685
1693
  function maybeRewriteExplicitInspectionPrompt(promptText, readOnlyRoots = []) {
1686
1694
  const text = String(promptText || "").trim();
1687
1695
  if (readOnlyRoots.length === 0) return text;
@@ -3308,8 +3316,10 @@ async function runTextTurnInteractive({
3308
3316
  spinnerLabel = "thinking..."
3309
3317
  }) {
3310
3318
  const readOnlyRoots = extractExplicitReadOnlyRoots(promptText, context.cwd);
3319
+ const writeRoots = extractExplicitWriteRoots(promptText, context.cwd);
3311
3320
  const effectivePromptText = maybeRewriteExplicitInspectionPrompt(promptText, readOnlyRoots);
3312
3321
  agent.toolRuntime.setReadOnlyRoots(readOnlyRoots);
3322
+ agent.toolRuntime.setWriteRoots(writeRoots);
3313
3323
  await maybeAutoCompactConversation({
3314
3324
  agent,
3315
3325
  currentSession,
@@ -3459,6 +3469,7 @@ async function runTextTurnInteractive({
3459
3469
  detachInterruptListener();
3460
3470
  spinner.stop();
3461
3471
  agent.toolRuntime.setReadOnlyRoots([]);
3472
+ agent.toolRuntime.setWriteRoots([]);
3462
3473
  }
3463
3474
 
3464
3475
  context.lastUsage = response?.usage || null;
package/src/path-utils.js CHANGED
@@ -39,8 +39,8 @@ function isAllowedReadOnlyHomePath(targetPath) {
39
39
  return COMMON_READ_ONLY_HOME_ROOTS.some((rootPath) => isWithinPath(rootPath, targetPath));
40
40
  }
41
41
 
42
- function isAllowedExplicitReadOnlyPath(targetPath, allowedReadOnlyRoots = []) {
43
- return allowedReadOnlyRoots.some((rootPath) => {
42
+ function isAllowedExplicitPath(targetPath, allowedRoots = []) {
43
+ return allowedRoots.some((rootPath) => {
44
44
  if (!rootPath) return false;
45
45
  const resolvedRoot = canonicalizePath(String(rootPath));
46
46
  return isWithinPath(resolvedRoot, targetPath);
@@ -65,11 +65,14 @@ export function resolveSandboxPath(baseDir, targetPath, allowOutsideCwd = false,
65
65
  const rel = path.relative(baseReal, resolved);
66
66
  const outside = rel.startsWith("..") || path.isAbsolute(rel);
67
67
  const allowExplicitReadOnly =
68
- Array.isArray(options.allowedReadOnlyRoots) && isAllowedExplicitReadOnlyPath(resolved, options.allowedReadOnlyRoots);
68
+ Array.isArray(options.allowedReadOnlyRoots) && isAllowedExplicitPath(resolved, options.allowedReadOnlyRoots);
69
+ const allowExplicitWrite =
70
+ Array.isArray(options.allowedWriteRoots) && isAllowedExplicitPath(resolved, options.allowedWriteRoots);
69
71
  if (
70
72
  outside &&
71
73
  !(options.allowReadOnlyHome === true && isAllowedReadOnlyHomePath(resolved)) &&
72
- !allowExplicitReadOnly
74
+ !allowExplicitReadOnly &&
75
+ !allowExplicitWrite
73
76
  ) {
74
77
  throw new Error(`Path is outside cwd sandbox: ${targetPath}`);
75
78
  }
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 { resolveSandboxPath } from "./path-utils.js";
8
+ import { expandHomePath, resolveSandboxPath } from "./path-utils.js";
9
9
  import { promptKeyChoice, promptLine } from "./prompt.js";
10
10
  import { McpManager } from "./mcp.js";
11
11
 
@@ -394,8 +394,8 @@ function globToRegExp(glob) {
394
394
  }
395
395
 
396
396
  function matchesPattern(value, pattern) {
397
- const normalizedValue = String(value || "").replace(/^\.\//, "");
398
- const normalizedPattern = String(pattern || "").replace(/^\.\//, "");
397
+ const normalizedValue = normalizePathPattern(value);
398
+ const normalizedPattern = normalizePathPattern(pattern);
399
399
  return globToRegExp(normalizedPattern).test(normalizedValue);
400
400
  }
401
401
 
@@ -409,7 +409,14 @@ function extractPatchPaths(patchText) {
409
409
  }
410
410
 
411
411
  function normalizePathList(values = []) {
412
- return uniqueStrings(values).map((item) => item.replace(/^\.\//, "").replace(/\\/g, "/"));
412
+ return uniqueStrings(values).map((item) => normalizePathPattern(item));
413
+ }
414
+
415
+ function normalizePathPattern(value) {
416
+ const raw = String(value || "").trim();
417
+ if (!raw) return "";
418
+ const expanded = expandHomePath(raw);
419
+ return String(expanded).replace(/^\.\//, "").replace(/\\/g, "/");
413
420
  }
414
421
 
415
422
  function looksLikeMutatingShellCommand(command) {
@@ -891,6 +898,7 @@ export function createToolRuntime({
891
898
  let checkpointCreatedThisTurn = false;
892
899
  let currentTurn = buildDefaultTurnRecord();
893
900
  let currentReadOnlyRoots = [];
901
+ let currentWriteRoots = [];
894
902
  let currentExperienceMode = normalizeExperienceMode(experienceMode);
895
903
  let currentAutonomyMode = normalizeAutonomyMode(autonomyMode);
896
904
  let currentRequireTurnContracts = requireTurnContracts !== false;
@@ -1326,7 +1334,9 @@ ${clipped}`;
1326
1334
  }
1327
1335
 
1328
1336
  async function writeFile(args = {}) {
1329
- const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1337
+ const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1338
+ allowedWriteRoots: currentWriteRoots
1339
+ });
1330
1340
  await fs.mkdir(path.dirname(filePath), { recursive: true });
1331
1341
  if (args.mode === "append") {
1332
1342
  await fs.appendFile(filePath, args.content, "utf8");
@@ -1383,7 +1393,9 @@ ${clipped}`;
1383
1393
  }
1384
1394
 
1385
1395
  async function replaceInFile(args = {}) {
1386
- const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1396
+ const filePath = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1397
+ allowedWriteRoots: currentWriteRoots
1398
+ });
1387
1399
  const source = await fs.readFile(filePath, "utf8");
1388
1400
 
1389
1401
  let next;
@@ -1564,13 +1576,17 @@ ${clipped}`;
1564
1576
  }
1565
1577
 
1566
1578
  async function makeDirectory(args = {}) {
1567
- const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1579
+ const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1580
+ allowedWriteRoots: currentWriteRoots
1581
+ });
1568
1582
  await fs.mkdir(target, { recursive: true });
1569
1583
  return pretty({ ok: true, path: args.path });
1570
1584
  }
1571
1585
 
1572
1586
  async function deletePath(args = {}) {
1573
- const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd);
1587
+ const target = resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1588
+ allowedWriteRoots: currentWriteRoots
1589
+ });
1574
1590
  if (target === cwd) throw new Error("Refusing to delete cwd root");
1575
1591
  await fs.rm(target, { recursive: Boolean(args.recursive), force: false });
1576
1592
  return pretty({ ok: true, path: args.path, recursive: Boolean(args.recursive) });
@@ -2181,6 +2197,8 @@ ${clipped}`;
2181
2197
  getShellCwd,
2182
2198
  setReadOnlyRoots(paths = []) { currentReadOnlyRoots = normalizePathList(paths); },
2183
2199
  getReadOnlyRoots() { return [...currentReadOnlyRoots]; },
2200
+ setWriteRoots(paths = []) { currentWriteRoots = normalizePathList(paths); },
2201
+ getWriteRoots() { return [...currentWriteRoots]; },
2184
2202
  setExperienceMode(mode) { currentExperienceMode = normalizeExperienceMode(mode); },
2185
2203
  getExperienceMode() { return currentExperienceMode; },
2186
2204
  setAutonomyMode(mode) { currentAutonomyMode = normalizeAutonomyMode(mode); },