@getollie/cli 0.2.0 → 0.2.2

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/dist/cli.js CHANGED
@@ -7,8 +7,12 @@ import { CommanderError } from "commander";
7
7
  import { Command } from "commander";
8
8
 
9
9
  // src/config.ts
10
- import { access, stat } from "fs/promises";
11
- import { resolve } from "path";
10
+ import { access, realpath, stat } from "fs/promises";
11
+ import { posix as posix2, resolve as resolve2 } from "path";
12
+
13
+ // src/sandbox/mounts.ts
14
+ import { realpathSync, statSync } from "fs";
15
+ import { posix, resolve } from "path";
12
16
 
13
17
  // src/utils/errors.ts
14
18
  var CliError = class extends Error {
@@ -26,6 +30,68 @@ function getErrorMessage(error) {
26
30
  return String(error);
27
31
  }
28
32
 
33
+ // src/sandbox/mounts.ts
34
+ var SANDBOX_WORKSPACE_PATH = "/workspace";
35
+ function sandboxPathsOverlap(left, right) {
36
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
37
+ }
38
+ function sandboxPathContains(parent, child) {
39
+ return parent === child || child.startsWith(`${parent}/`);
40
+ }
41
+ function validateSandboxPath(path, label = "Sandbox path") {
42
+ if (!posix.isAbsolute(path) || path.includes("\\") || path.includes("\0") || path.split("/").includes("..") || posix.normalize(path) !== path) {
43
+ throw new CliError(`${label} must be a normalized absolute sandbox path: ${path}`);
44
+ }
45
+ return path;
46
+ }
47
+ function validateMountPoint(mountPoint) {
48
+ validateSandboxPath(mountPoint, "Mount point");
49
+ if (mountPoint === "/") {
50
+ throw new CliError(`Mount point must be a normalized absolute sandbox path other than /: ${mountPoint}`);
51
+ }
52
+ }
53
+ function validateSandboxMounts(mounts, options = {}) {
54
+ const reservedMountPoint = options.reservedMountPoint === void 0 ? SANDBOX_WORKSPACE_PATH : options.reservedMountPoint;
55
+ const validated = [];
56
+ for (const mount of mounts) {
57
+ validateMountPoint(mount.mountPoint);
58
+ if (reservedMountPoint && sandboxPathsOverlap(mount.mountPoint, reservedMountPoint)) {
59
+ throw new CliError(`Mount point must be outside ${reservedMountPoint}: ${mount.mountPoint}`);
60
+ }
61
+ let hostPath;
62
+ try {
63
+ hostPath = realpathSync(resolve(mount.hostPath));
64
+ if (!statSync(hostPath).isDirectory()) throw new Error("not a directory");
65
+ } catch {
66
+ throw new CliError(
67
+ `Mount source host path does not exist or is not a directory: ${resolve(mount.hostPath)}`
68
+ );
69
+ }
70
+ const conflict = validated.find(
71
+ (candidate) => sandboxPathsOverlap(candidate.mountPoint, mount.mountPoint)
72
+ );
73
+ if (conflict) {
74
+ throw new CliError(
75
+ `Mount points must not overlap: ${conflict.mountPoint} and ${mount.mountPoint}`
76
+ );
77
+ }
78
+ validated.push({ hostPath, mountPoint: mount.mountPoint });
79
+ }
80
+ return validated;
81
+ }
82
+ function parseSandboxMounts(specs = [], options) {
83
+ const mounts = specs.map((spec) => {
84
+ const separator = spec.lastIndexOf(":");
85
+ if (separator <= 0 || separator === spec.length - 1) {
86
+ throw new CliError(
87
+ `Invalid mount ${JSON.stringify(spec)}. Expected <host-path>:<sandbox-path>`
88
+ );
89
+ }
90
+ return { hostPath: spec.slice(0, separator), mountPoint: spec.slice(separator + 1) };
91
+ });
92
+ return validateSandboxMounts(mounts, options);
93
+ }
94
+
29
95
  // src/config.ts
30
96
  var DEFAULT_PROVIDER = "openai";
31
97
  var DEFAULT_MODEL = "gpt-5.6";
@@ -34,9 +100,7 @@ var DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
34
100
  var DEFAULT_MAX_OUTPUT_BYTES = 65536;
35
101
  var DEFAULT_EXECUTOR = "none";
36
102
  function parseNumber(name, value, fallback) {
37
- if (value === void 0 || value === "") {
38
- return fallback;
39
- }
103
+ if (value === void 0 || value === "") return fallback;
40
104
  const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
41
105
  if (!Number.isFinite(parsed) || parsed < 0) {
42
106
  throw new CliError(`${name} must be a non-negative integer`);
@@ -53,39 +117,82 @@ function parseExecutor(value) {
53
117
  }
54
118
  return executor;
55
119
  }
56
- async function resolveRunConfig(input = {}, env = process.env) {
57
- const cwd = resolve(input.cwd ?? env.OLLIE_CWD ?? process.cwd());
58
- const stats = await stat(cwd).catch(() => null);
120
+ async function resolveHostDirectory(value, label) {
121
+ const absolute = resolve2(value);
122
+ const stats = await stat(absolute).catch(() => null);
59
123
  if (!stats?.isDirectory()) {
60
- throw new CliError(`Working directory does not exist or is not a directory: ${cwd}`);
124
+ throw new CliError(`${label} host path does not exist or is not a directory: ${absolute}`);
61
125
  }
62
- await access(cwd);
126
+ await access(absolute);
127
+ return realpath(absolute);
128
+ }
129
+ async function resolveRunConfig(input = {}, env = process.env) {
130
+ const configuredHostCwd = input.cwd ?? env.OLLIE_CWD;
131
+ const configuredSandboxCwd = input.sandboxCwd ?? env.OLLIE_SANDBOX_CWD;
132
+ const usedImplicitCwd = configuredHostCwd === void 0 && configuredSandboxCwd === void 0;
133
+ const hostCwdValue = configuredHostCwd ?? (usedImplicitCwd ? process.cwd() : void 0);
134
+ const explicitMounts = parseSandboxMounts(input.mount, {
135
+ reservedMountPoint: hostCwdValue ? "/workspace" : null
136
+ });
137
+ let workspace;
138
+ let mounts;
139
+ let sandboxCwd;
140
+ if (hostCwdValue) {
141
+ workspace = {
142
+ hostPath: await resolveHostDirectory(hostCwdValue, "Working directory"),
143
+ mountPoint: "/workspace"
144
+ };
145
+ mounts = explicitMounts;
146
+ sandboxCwd = configuredSandboxCwd ?? "/workspace";
147
+ } else {
148
+ if (!configuredSandboxCwd) {
149
+ throw new CliError("Either --cwd or --sandbox-cwd must be specified.");
150
+ }
151
+ sandboxCwd = configuredSandboxCwd;
152
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
153
+ const containing2 = explicitMounts.find(
154
+ (mount) => sandboxPathContains(mount.mountPoint, sandboxCwd)
155
+ );
156
+ if (!containing2) {
157
+ const destinations = explicitMounts.map((mount) => mount.mountPoint).join(", ") || "(none)";
158
+ throw new CliError(
159
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount. Configured mount destinations: ${destinations}`
160
+ );
161
+ }
162
+ workspace = containing2;
163
+ mounts = explicitMounts.filter((mount) => mount !== containing2);
164
+ }
165
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
166
+ const allMounts = [workspace, ...mounts];
167
+ const containing = allMounts.find((mount) => sandboxPathContains(mount.mountPoint, sandboxCwd));
168
+ if (!containing) {
169
+ const destinations = allMounts.map((mount) => mount.mountPoint).join(", ");
170
+ throw new CliError(
171
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount. Configured mount destinations: ${destinations}`
172
+ );
173
+ }
174
+ const mappedRelative = posix2.relative(containing.mountPoint, sandboxCwd);
175
+ await resolveHostDirectory(resolve2(containing.hostPath, mappedRelative), "Sandbox cwd mapped");
176
+ const workspaceRelativeCwd = sandboxPathContains(workspace.mountPoint, sandboxCwd) ? posix2.relative(workspace.mountPoint, sandboxCwd) || "." : null;
63
177
  const executor = parseExecutor(input.executor ?? env.OLLIE_EXECUTOR);
64
178
  return {
65
- cwd,
179
+ cwd: workspace.hostPath,
180
+ workspaceMountPoint: workspace.mountPoint,
181
+ sandboxCwd,
182
+ workspaceRelativeCwd,
183
+ mounts,
184
+ usedImplicitCwd,
66
185
  provider: input.provider ?? env.OLLIE_PROVIDER ?? DEFAULT_PROVIDER,
67
186
  model: input.model ?? env.OLLIE_MODEL ?? DEFAULT_MODEL,
68
187
  executor,
69
- maxSteps: clamp(
70
- parseNumber("max steps", input.maxSteps ?? env.OLLIE_MAX_STEPS, DEFAULT_MAX_STEPS),
71
- 1,
72
- 100
73
- ),
188
+ maxSteps: clamp(parseNumber("max steps", input.maxSteps ?? env.OLLIE_MAX_STEPS, DEFAULT_MAX_STEPS), 1, 100),
74
189
  commandTimeoutMs: clamp(
75
- parseNumber(
76
- "command timeout",
77
- input.timeout ?? env.OLLIE_COMMAND_TIMEOUT_MS,
78
- DEFAULT_COMMAND_TIMEOUT_MS
79
- ),
190
+ parseNumber("command timeout", input.timeout ?? env.OLLIE_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS),
80
191
  1,
81
192
  3e5
82
193
  ),
83
194
  maxOutputBytes: clamp(
84
- parseNumber(
85
- "max output",
86
- input.maxOutput ?? env.OLLIE_MAX_OUTPUT_BYTES,
87
- DEFAULT_MAX_OUTPUT_BYTES
88
- ),
195
+ parseNumber("max output", input.maxOutput ?? env.OLLIE_MAX_OUTPUT_BYTES, DEFAULT_MAX_OUTPUT_BYTES),
89
196
  1,
90
197
  2e6
91
198
  ),
@@ -99,23 +206,23 @@ async function resolveRunConfig(input = {}, env = process.env) {
99
206
  import { access as access2, stat as stat2 } from "fs/promises";
100
207
 
101
208
  // src/sandbox/just-bash.ts
102
- import { realpathSync, statSync } from "fs";
209
+ import { realpathSync as realpathSync2, statSync as statSync2 } from "fs";
103
210
  import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from "just-bash";
104
211
 
105
212
  // src/sandbox/git/index.ts
106
- import { posix as posix2 } from "path";
213
+ import { posix as posix4 } from "path";
107
214
  import { createTwoFilesPatch } from "diff";
108
215
  import * as git from "isomorphic-git";
109
216
  import { defineCommand } from "just-bash";
110
217
 
111
218
  // src/sandbox/git/fs-adapter.ts
112
- import { posix } from "path";
219
+ import { posix as posix3 } from "path";
113
220
  var ROOT = "/workspace";
114
221
  function safePath(path) {
115
222
  if (path.includes("\0") || path.includes("\\")) {
116
223
  throw Object.assign(new Error("Invalid virtual Git path"), { code: "EINVAL" });
117
224
  }
118
- const normalized = posix.resolve(path);
225
+ const normalized = posix3.resolve(path);
119
226
  if (normalized !== ROOT && !normalized.startsWith(`${ROOT}/`)) {
120
227
  throw Object.assign(new Error("Git path is outside /workspace"), { code: "EACCES" });
121
228
  }
@@ -135,7 +242,7 @@ function normalizeError(error) {
135
242
  throw Object.assign(new Error("Virtual filesystem operation failed"), { code: "EIO" });
136
243
  }
137
244
  async function ensureParent(fs, path) {
138
- const parent = posix.dirname(path);
245
+ const parent = posix3.dirname(path);
139
246
  if (!await fs.exists(parent)) await fs.mkdir(parent, { recursive: true });
140
247
  }
141
248
  function statShape(stat4) {
@@ -370,9 +477,9 @@ function virtualPath(ctx, input = ".") {
370
477
  async function repositoryDir(ctx) {
371
478
  let current = virtualPath(ctx);
372
479
  while (current === WORKSPACE || current.startsWith(`${WORKSPACE}/`)) {
373
- if (await ctx.fs.exists(posix2.join(current, ".git"))) return current;
480
+ if (await ctx.fs.exists(posix4.join(current, ".git"))) return current;
374
481
  if (current === WORKSPACE) break;
375
- current = posix2.dirname(current);
482
+ current = posix4.dirname(current);
376
483
  }
377
484
  throw new Error("not a git repository (or any parent up to /workspace)");
378
485
  }
@@ -455,7 +562,7 @@ async function writeGlobalConfig(ctx, values) {
455
562
  ([section, entries]) => `[${section}]
456
563
  ${entries.map(([key, value]) => ` ${key} = ${value}`).join("\n")}`
457
564
  ).join("\n");
458
- await ctx.fs.mkdir(posix2.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
565
+ await ctx.fs.mkdir(posix4.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
459
566
  await ctx.fs.writeFile(GLOBAL_CONFIG_PATH, content ? `${content}
460
567
  ` : "");
461
568
  }
@@ -469,7 +576,7 @@ async function handleInit(context, args) {
469
576
  throw new Error("cannot create repository directory");
470
577
  });
471
578
  await git.init({ fs: context.fs, dir, defaultBranch: branchOpt.value ?? "main" });
472
- return ok(`Initialized empty Git repository in ${posix2.join(dir, ".git")}/
579
+ return ok(`Initialized empty Git repository in ${posix4.join(dir, ".git")}/
473
580
  `);
474
581
  }
475
582
  async function handleStatus(context, args) {
@@ -622,7 +729,7 @@ async function handleDiff(context, args) {
622
729
  const changed = cachedFlag.enabled ? headStatus !== stageStatus : workdirStatus !== stageStatus;
623
730
  if (!changed) continue;
624
731
  const oldBytes = cachedFlag.enabled ? head && headStatus !== 0 ? (await git.readBlob({ fs: context.fs, dir, oid: head, filepath })).blob : empty : await readStage(filepath);
625
- const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix2.join(dir, filepath));
732
+ const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix4.join(dir, filepath));
626
733
  patches.push(patch(filepath, oldBytes, newBytes));
627
734
  }
628
735
  return ok(patches.join(""));
@@ -689,7 +796,7 @@ async function handleCheckout(context, args) {
689
796
  const oid = await git.resolveRef({ fs: context.fs, dir, ref });
690
797
  for (const filepath of filepaths) {
691
798
  const blob = await readCheckoutBlob(context, dir, oid, filepath);
692
- await context.command.fs.writeFile(posix2.join(dir, filepath), blob);
799
+ await context.command.fs.writeFile(posix4.join(dir, filepath), blob);
693
800
  }
694
801
  return ok();
695
802
  }
@@ -816,7 +923,7 @@ async function handleClone(context, args) {
816
923
  const [rawUrl, destination, ...rest] = noCheckoutFlag.rest;
817
924
  if (!rawUrl || rest.length) usage("clone requires a URL and optional directory");
818
925
  const url = validateRemoteUrl(rawUrl);
819
- const inferred = posix2.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
926
+ const inferred = posix4.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
820
927
  const dir = virtualPath(context.command, destination ?? inferred);
821
928
  const depth = positiveInteger(depthOpt.value, "--depth");
822
929
  await git.clone({
@@ -980,34 +1087,49 @@ Supported commands: ${COMMANDS.join(", ")}
980
1087
  }
981
1088
 
982
1089
  // src/sandbox/just-bash.ts
983
- var SANDBOX_WORKSPACE_PATH = "/workspace";
1090
+ var SANDBOX_WORKSPACE_PATH2 = "/workspace";
984
1091
  function canonicalizeWorkspaceRoot(workspaceRoot) {
985
- const canonicalRoot = realpathSync(workspaceRoot);
986
- if (!statSync(canonicalRoot).isDirectory()) {
1092
+ const canonicalRoot = realpathSync2(workspaceRoot);
1093
+ if (!statSync2(canonicalRoot).isDirectory()) {
987
1094
  throw new Error("The sandbox workspace root must be a directory");
988
1095
  }
989
1096
  return canonicalRoot;
990
1097
  }
991
- function createWorkspaceFs(workspaceRoot) {
1098
+ function createWorkspaceFs(workspaceRoot, options) {
992
1099
  const root = canonicalizeWorkspaceRoot(workspaceRoot);
1100
+ const workspaceMountPoint = options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2;
1101
+ validateSandboxPath(workspaceMountPoint, "Workspace mount point");
1102
+ const additionalMounts = validateSandboxMounts(options.mounts ?? [], {
1103
+ reservedMountPoint: workspaceMountPoint
1104
+ }).map((mount) => ({
1105
+ mountPoint: mount.mountPoint,
1106
+ filesystem: new ReadWriteFs({ root: mount.hostPath, allowSymlinks: false })
1107
+ }));
993
1108
  return new MountableFs({
994
1109
  base: new InMemoryFs(),
995
1110
  mounts: [
996
1111
  {
997
- mountPoint: SANDBOX_WORKSPACE_PATH,
1112
+ mountPoint: workspaceMountPoint,
998
1113
  filesystem: new ReadWriteFs({ root, allowSymlinks: false })
999
- }
1114
+ },
1115
+ ...additionalMounts
1000
1116
  ]
1001
1117
  });
1002
1118
  }
1003
1119
  function createJustBash(workspaceRoot, options = {}) {
1120
+ const workspaceMountPoint = options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2;
1121
+ const sandboxCwd = options.sandboxCwd ?? workspaceMountPoint;
1122
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
1123
+ const configuredMountPoints = [workspaceMountPoint, ...(options.mounts ?? []).map((m) => m.mountPoint)];
1124
+ if (!configuredMountPoints.some((mountPoint) => sandboxPathContains(mountPoint, sandboxCwd))) {
1125
+ throw new CliError(
1126
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount.`
1127
+ );
1128
+ }
1004
1129
  return new Bash({
1005
- fs: createWorkspaceFs(workspaceRoot),
1006
- cwd: SANDBOX_WORKSPACE_PATH,
1007
- env: {
1008
- HOME: "/home/user",
1009
- TERM: "xterm-256color"
1010
- },
1130
+ fs: createWorkspaceFs(workspaceRoot, options),
1131
+ cwd: sandboxCwd,
1132
+ env: { HOME: "/home/user", TERM: "xterm-256color" },
1011
1133
  python: true,
1012
1134
  javascript: true,
1013
1135
  customCommands: options.git === false ? [] : [createGitCommand(options.git)],
@@ -1104,7 +1226,11 @@ async function runExecCommand(cwd, command, options, signal) {
1104
1226
  const combinedSignal = combineSignals([signal, timeout.signal]);
1105
1227
  const timedOut = () => timeout.signal.aborted && !signal?.aborted;
1106
1228
  try {
1107
- const bash = createJustBash(cwd);
1229
+ const bash = createJustBash(cwd, {
1230
+ workspaceMountPoint: options.workspaceMountPoint,
1231
+ sandboxCwd: options.sandboxCwd,
1232
+ mounts: options.mounts
1233
+ });
1108
1234
  const result = await bash.exec(command, { env: options.env, signal: combinedSignal });
1109
1235
  return {
1110
1236
  ...result,
@@ -1353,7 +1479,7 @@ var ExecutorController = class {
1353
1479
  // src/executor/daytona.ts
1354
1480
  import { randomUUID } from "crypto";
1355
1481
  import { mkdir, readdir, readFile, rm, stat as stat3, writeFile } from "fs/promises";
1356
- import { dirname, isAbsolute, relative, resolve as resolve2, sep } from "path";
1482
+ import { dirname, isAbsolute, relative, resolve as resolve3, sep } from "path";
1357
1483
  import {
1358
1484
  Daytona,
1359
1485
  DaytonaProcessExecutionTimeoutError
@@ -1388,7 +1514,7 @@ async function listLocalFiles(root) {
1388
1514
  const files = /* @__PURE__ */ new Map();
1389
1515
  async function visit(directory) {
1390
1516
  for (const entry of await readdir(directory, { withFileTypes: true })) {
1391
- const absolutePath = resolve2(directory, entry.name);
1517
+ const absolutePath = resolve3(directory, entry.name);
1392
1518
  if (entry.isDirectory()) {
1393
1519
  await visit(absolutePath);
1394
1520
  } else if (entry.isFile()) {
@@ -1403,12 +1529,14 @@ async function listLocalFiles(root) {
1403
1529
  var DaytonaSandbox = class {
1404
1530
  #sandbox;
1405
1531
  #workspacePath;
1532
+ #workingDirectory;
1406
1533
  #commandTimeoutMs;
1407
1534
  #signal;
1408
1535
  #disposed = false;
1409
1536
  constructor(sandbox, options) {
1410
1537
  this.#sandbox = sandbox;
1411
1538
  this.#workspacePath = options.workspacePath;
1539
+ this.#workingDirectory = options.workingDirectory ?? options.workspacePath;
1412
1540
  this.#commandTimeoutMs = options.commandTimeoutMs;
1413
1541
  this.#signal = options.signal;
1414
1542
  }
@@ -1425,7 +1553,7 @@ ${marker}
1425
1553
  const response = await abortable(
1426
1554
  this.#sandbox.process.executeCommand(
1427
1555
  wrapped,
1428
- this.#workspacePath,
1556
+ this.#workingDirectory,
1429
1557
  void 0,
1430
1558
  timeoutSeconds(this.#commandTimeoutMs)
1431
1559
  ),
@@ -1472,7 +1600,7 @@ ${marker}
1472
1600
  this.#disposed = true;
1473
1601
  }
1474
1602
  #resolvePath(path) {
1475
- const target = isAbsolute(path) ? resolve2(path) : resolve2(this.#workspacePath, path);
1603
+ const target = isAbsolute(path) ? resolve3(path) : resolve3(this.#workingDirectory, path);
1476
1604
  if (target !== this.#workspacePath && !target.startsWith(`${this.#workspacePath}/`)) {
1477
1605
  throw new Error(`Path is outside the Daytona workspace: ${path}`);
1478
1606
  }
@@ -1488,7 +1616,7 @@ var DaytonaWorkspaceSynchronizer = class {
1488
1616
  #sandbox;
1489
1617
  #knownFiles = /* @__PURE__ */ new Set();
1490
1618
  constructor(cwd, remoteWorkspacePath, sandbox) {
1491
- this.#cwd = resolve2(cwd);
1619
+ this.#cwd = resolve3(cwd);
1492
1620
  this.#remoteWorkspacePath = remoteWorkspacePath;
1493
1621
  this.#sandbox = sandbox;
1494
1622
  }
@@ -1519,7 +1647,7 @@ var DaytonaWorkspaceSynchronizer = class {
1519
1647
  for (const [path, size] of localFiles) {
1520
1648
  signal?.throwIfAborted();
1521
1649
  await abortable(
1522
- this.#sandbox.fs.uploadFile(resolve2(this.#cwd, path), this.#remotePath(path)),
1650
+ this.#sandbox.fs.uploadFile(resolve3(this.#cwd, path), this.#remotePath(path)),
1523
1651
  signal
1524
1652
  );
1525
1653
  if (this.#knownFiles.has(path)) filesUpdated++;
@@ -1551,7 +1679,7 @@ var DaytonaWorkspaceSynchronizer = class {
1551
1679
  let bytesTransferred = 0;
1552
1680
  for (const path of this.#knownFiles) {
1553
1681
  if (remoteFiles.has(path)) continue;
1554
- await rm(resolve2(this.#cwd, path), { force: true });
1682
+ await rm(resolve3(this.#cwd, path), { force: true });
1555
1683
  filesDeleted++;
1556
1684
  }
1557
1685
  for (const path of remoteFiles) {
@@ -1581,7 +1709,7 @@ var DaytonaWorkspaceSynchronizer = class {
1581
1709
  return `${this.#remoteWorkspacePath}/${path}`;
1582
1710
  }
1583
1711
  #localPath(path) {
1584
- const target = resolve2(this.#cwd, path);
1712
+ const target = resolve3(this.#cwd, path);
1585
1713
  if (target !== this.#cwd && !target.startsWith(`${this.#cwd}${sep}`)) {
1586
1714
  throw new Error(`Daytona returned a path outside the workspace: ${path}`);
1587
1715
  }
@@ -1606,16 +1734,24 @@ var DaytonaExecutorSession = class {
1606
1734
  #disposed = false;
1607
1735
  constructor(remote, options, signal) {
1608
1736
  const workspacePath = options.remoteWorkspacePath ?? DEFAULT_REMOTE_WORKSPACE;
1737
+ const relativeCwd = options.relativeCwd ?? ".";
1738
+ const workingDirectory = resolve3(workspacePath, relativeCwd);
1609
1739
  this.#remote = remote;
1610
1740
  this.#synchronizer = new DaytonaWorkspaceSynchronizer(options.cwd, workspacePath, remote);
1611
1741
  this.sandbox = new DaytonaSandbox(remote, {
1612
1742
  workspacePath,
1743
+ workingDirectory,
1613
1744
  commandTimeoutMs: options.commandTimeoutMs,
1614
1745
  signal
1615
1746
  });
1616
1747
  this.context = {
1617
1748
  kind: "daytona",
1618
1749
  workspacePath,
1750
+ ...options.relativeCwd !== void 0 ? {
1751
+ workingDirectory,
1752
+ cwdPreserved: options.relativeCwd !== null,
1753
+ ...options.relativeCwd === null ? { cwdLimitation: "The selected sandbox cwd is on an additional mount; starting at the effective workspace root." } : {}
1754
+ } : {},
1619
1755
  isolated: true,
1620
1756
  networkAccess: "full"
1621
1757
  };
@@ -1715,7 +1851,7 @@ Terminus-2 spirit:
1715
1851
  - Summarize final findings and important command results clearly.
1716
1852
 
1717
1853
  Runtime context:
1718
- - Working directory: ${SANDBOX_WORKSPACE_PATH}
1854
+ - Working directory: ${options.sandboxCwd ?? "/workspace"}
1719
1855
  - Current time: ${options.now.toISOString()}
1720
1856
  - Maximum model/tool steps: ${options.maxSteps}
1721
1857
  - Per-command timeout: ${options.commandTimeoutMs} ms
@@ -1738,17 +1874,20 @@ Tool policy:
1738
1874
  }
1739
1875
  function buildDelegatedSystemPrompt(options) {
1740
1876
  const executionBoundary = options.context.isolated ? "Commands run natively inside an isolated remote environment." : "Commands run natively with the current user's permissions and are not isolated.";
1877
+ const cwdNote = options.context.cwdLimitation ? `
1878
+ - Working-directory limitation: ${options.context.cwdLimitation}` : "";
1741
1879
  return `You are Ollie's delegated execution agent. Complete one bounded task using native workspace tools.
1742
1880
 
1743
1881
  Runtime context:
1744
1882
  - Workspace root: ${options.context.workspacePath}
1883
+ - Working directory: ${options.context.workingDirectory ?? options.context.workspacePath}${cwdNote}
1745
1884
  - Current time: ${options.now.toISOString()}
1746
1885
  - Maximum model/tool steps: ${options.maxSteps}
1747
1886
  - Per-command timeout: ${options.commandTimeoutMs} ms
1748
1887
  - ${executionBoundary}
1749
1888
 
1750
1889
  Operating rules:
1751
- - Start at the workspace root and use relative paths for workspace files.
1890
+ - Start at the configured working directory and use relative paths for workspace files.
1752
1891
  - Inspect the relevant state before making changes.
1753
1892
  - Keep changes minimal and directly related to the delegated task.
1754
1893
  - Run appropriate native checks or tests and report their actual results.
@@ -1770,8 +1909,12 @@ async function createWorkspaceTools(options) {
1770
1909
  }
1771
1910
  async function createTools(options) {
1772
1911
  return createWorkspaceTools({
1773
- sandbox: createJustBash(options.cwd),
1774
- destination: SANDBOX_WORKSPACE_PATH,
1912
+ sandbox: createJustBash(options.cwd, {
1913
+ mounts: options.mounts,
1914
+ workspaceMountPoint: options.workspaceMountPoint,
1915
+ sandboxCwd: options.sandboxCwd
1916
+ }),
1917
+ destination: options.sandboxCwd ?? options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2,
1775
1918
  maxOutputBytes: options.maxOutputBytes,
1776
1919
  extraInstructions: "Use bash commands to inspect files, search, run tests, and gather terminal state. Sandbox-native git is available; local Git changes persist in the workspace, and git push has permanent remote effects, so destructive actions and pushes must only be used when explicitly requested. Keep commands bounded."
1777
1920
  });
@@ -1796,9 +1939,9 @@ var ModelTaskDelegator = class {
1796
1939
  });
1797
1940
  const tools = await createWorkspaceTools({
1798
1941
  sandbox: session.sandbox,
1799
- destination: session.context.workspacePath,
1942
+ destination: session.context.workingDirectory ?? session.context.workspacePath,
1800
1943
  maxOutputBytes: this.options.maxOutputBytes,
1801
- extraInstructions: "Use the native workspace tools to complete the delegated task. Start at the workspace root, use relative paths, keep changes focused, and verify the result before reporting it."
1944
+ extraInstructions: "Use the native workspace tools to complete the delegated task. Use relative paths from the configured working directory, keep changes focused, and verify the result before reporting it."
1802
1945
  });
1803
1946
  const result = await generateText({
1804
1947
  model: this.options.model,
@@ -1846,7 +1989,7 @@ var ModelTaskDelegator = class {
1846
1989
  // src/executor/local.ts
1847
1990
  import { spawn } from "child_process";
1848
1991
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1849
- import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
1992
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
1850
1993
  var LocalSandbox = class {
1851
1994
  #cwd;
1852
1995
  #commandTimeoutMs;
@@ -1855,7 +1998,7 @@ var LocalSandbox = class {
1855
1998
  #processes = /* @__PURE__ */ new Set();
1856
1999
  #disposed = false;
1857
2000
  constructor(options, signal) {
1858
- this.#cwd = resolve3(options.cwd);
2001
+ this.#cwd = resolve4(options.cwd, options.relativeCwd ?? ".");
1859
2002
  this.#commandTimeoutMs = options.commandTimeoutMs;
1860
2003
  this.#env = options.env ?? process.env;
1861
2004
  this.#signal = signal;
@@ -1968,7 +2111,7 @@ var LocalSandbox = class {
1968
2111
  this.#processes.clear();
1969
2112
  }
1970
2113
  #resolvePath(path) {
1971
- return isAbsolute2(path) ? path : resolve3(this.#cwd, path);
2114
+ return isAbsolute2(path) ? path : resolve4(this.#cwd, path);
1972
2115
  }
1973
2116
  #assertActive() {
1974
2117
  if (this.#disposed) throw new Error("Local executor session has been disposed");
@@ -1979,11 +2122,17 @@ var LocalExecutorSession = class {
1979
2122
  context;
1980
2123
  #disposed = false;
1981
2124
  constructor(options, signal) {
1982
- const workspacePath = resolve3(options.cwd);
2125
+ const workspacePath = resolve4(options.cwd);
2126
+ const workingDirectory = resolve4(workspacePath, options.relativeCwd ?? ".");
1983
2127
  this.sandbox = new LocalSandbox(options, signal);
1984
2128
  this.context = {
1985
2129
  kind: "local",
1986
2130
  workspacePath,
2131
+ ...options.relativeCwd !== void 0 ? {
2132
+ workingDirectory,
2133
+ cwdPreserved: options.relativeCwd !== null,
2134
+ ...options.relativeCwd === null ? { cwdLimitation: "The selected sandbox cwd is on an additional mount; starting at the effective workspace root." } : {}
2135
+ } : {},
1987
2136
  isolated: false,
1988
2137
  networkAccess: "full"
1989
2138
  };
@@ -2077,11 +2226,13 @@ function createExecutorProvider(options) {
2077
2226
  case "local":
2078
2227
  return new LocalExecutorProvider({
2079
2228
  cwd: options.cwd,
2229
+ relativeCwd: options.workspaceRelativeCwd,
2080
2230
  commandTimeoutMs: options.commandTimeoutMs
2081
2231
  });
2082
2232
  case "daytona":
2083
2233
  return new DaytonaExecutorProvider({
2084
2234
  cwd: options.cwd,
2235
+ relativeCwd: options.workspaceRelativeCwd,
2085
2236
  commandTimeoutMs: options.commandTimeoutMs,
2086
2237
  snapshot: process.env.OLLIE_DAYTONA_SNAPSHOT
2087
2238
  });
@@ -2096,6 +2247,9 @@ async function runOneShot(options) {
2096
2247
  });
2097
2248
  const workspaceTools = await createTools({
2098
2249
  cwd: options.cwd,
2250
+ workspaceMountPoint: options.workspaceMountPoint,
2251
+ sandboxCwd: options.sandboxCwd,
2252
+ mounts: options.mounts,
2099
2253
  maxOutputBytes: options.maxOutputBytes
2100
2254
  });
2101
2255
  const executorProvider = createExecutorProvider(options);
@@ -2122,6 +2276,7 @@ async function runOneShot(options) {
2122
2276
  now: /* @__PURE__ */ new Date(),
2123
2277
  maxSteps: options.maxSteps,
2124
2278
  commandTimeoutMs: options.commandTimeoutMs,
2279
+ sandboxCwd: options.sandboxCwd,
2125
2280
  executorInstructions: executorProvider?.systemPrompt
2126
2281
  }),
2127
2282
  prompt: options.prompt,
@@ -2173,7 +2328,7 @@ async function runOneShot(options) {
2173
2328
 
2174
2329
  // src/sandbox/runtime.ts
2175
2330
  import { once } from "events";
2176
- import { posix as posix3 } from "path";
2331
+ import { posix as posix5 } from "path";
2177
2332
  import { createInterface } from "readline";
2178
2333
  function isRecord(value) {
2179
2334
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2211,7 +2366,7 @@ function requireString(params, name) {
2211
2366
  return value;
2212
2367
  }
2213
2368
  function validateVirtualPath(path) {
2214
- if (!posix3.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix3.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2369
+ if (!posix5.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix5.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2215
2370
  throw new Error("Path must be a normalized absolute virtual path");
2216
2371
  }
2217
2372
  return path;
@@ -2272,7 +2427,11 @@ async function runWorker(options = {}) {
2272
2427
  const workspaceRoot = options.cwd ?? process.cwd();
2273
2428
  const input = options.input ?? process.stdin;
2274
2429
  const output = options.output ?? process.stdout;
2275
- const bash = createJustBash(workspaceRoot);
2430
+ const bash = createJustBash(workspaceRoot, {
2431
+ mounts: options.mounts,
2432
+ workspaceMountPoint: options.workspaceMountPoint,
2433
+ sandboxCwd: options.sandboxCwd
2434
+ });
2276
2435
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
2277
2436
  const close = () => lines.close();
2278
2437
  if (options.signal?.aborted) {
@@ -2321,7 +2480,17 @@ function truncateText(input, maxBytes) {
2321
2480
 
2322
2481
  // src/cli-program.ts
2323
2482
  function addExecOptions(command, ndjsonDescription = "emit one JSON result record") {
2324
- return command.option("--cwd <path>", "working directory").option("--timeout <ms>", "command timeout in milliseconds").option("--max-output <bytes>", "maximum retained output").option("--ndjson", ndjsonDescription);
2483
+ return addMountOption(command.option("--cwd <host-path>", "host workspace mounted at /workspace").option("--sandbox-cwd <sandbox-path>", "initial working directory inside the sandbox")).option("--timeout <ms>", "command timeout in milliseconds").option("--max-output <bytes>", "maximum retained output").option("--ndjson", ndjsonDescription);
2484
+ }
2485
+ function collectOption(value, previous) {
2486
+ return [...previous ?? [], value];
2487
+ }
2488
+ function addMountOption(command) {
2489
+ return command.option(
2490
+ "--mount <host-path:sandbox-path>",
2491
+ "mount a host directory read-write (repeatable)",
2492
+ collectOption
2493
+ );
2325
2494
  }
2326
2495
  function addAgentOptions(command) {
2327
2496
  return addExecOptions(command, "emit newline-delimited JSON event records").option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option(
@@ -2330,12 +2499,20 @@ function addAgentOptions(command) {
2330
2499
  "none"
2331
2500
  ).option("--max-steps <number>", "maximum model/tool steps").option("--no-color", "disable color output");
2332
2501
  }
2502
+ function warnImplicitCwd(usedImplicitCwd) {
2503
+ if (usedImplicitCwd) {
2504
+ process.stderr.write(
2505
+ "Warning: omitting --cwd and --sandbox-cwd is deprecated; currently using --cwd .\n"
2506
+ );
2507
+ }
2508
+ }
2333
2509
  async function runAgent(promptParts, options) {
2334
2510
  const prompt = promptParts.join(" ").trim();
2335
2511
  if (!prompt) {
2336
2512
  throw new CliError('A prompt is required. Example: ollie agent "summarize this repository"');
2337
2513
  }
2338
2514
  const config = await resolveRunConfig(options);
2515
+ warnImplicitCwd(config.usedImplicitCwd);
2339
2516
  const renderer = createRenderer({ ndjson: config.ndjson, color: config.color });
2340
2517
  const controller = new AbortController();
2341
2518
  const onSigint = () => controller.abort();
@@ -2358,6 +2535,7 @@ async function runExec(commandParts, options) {
2358
2535
  throw new CliError('A command is required. Example: ollie "pwd && ls -la"');
2359
2536
  }
2360
2537
  const config = await resolveRunConfig(options);
2538
+ warnImplicitCwd(config.usedImplicitCwd);
2361
2539
  const controller = new AbortController();
2362
2540
  let signalExitCode;
2363
2541
  const onSigint = () => {
@@ -2376,7 +2554,10 @@ async function runExec(commandParts, options) {
2376
2554
  shellCommand,
2377
2555
  {
2378
2556
  timeoutMs: config.commandTimeoutMs,
2379
- env: process.env
2557
+ env: process.env,
2558
+ workspaceMountPoint: config.workspaceMountPoint,
2559
+ sandboxCwd: config.sandboxCwd,
2560
+ mounts: config.mounts
2380
2561
  },
2381
2562
  controller.signal
2382
2563
  );
@@ -2415,6 +2596,8 @@ async function runExec(commandParts, options) {
2415
2596
  }
2416
2597
  }
2417
2598
  async function runSandbox(options) {
2599
+ const config = await resolveRunConfig(options);
2600
+ warnImplicitCwd(config.usedImplicitCwd);
2418
2601
  const controller = new AbortController();
2419
2602
  let signalExitCode;
2420
2603
  const onSigint = () => {
@@ -2429,7 +2612,10 @@ async function runSandbox(options) {
2429
2612
  process.once("SIGTERM", onSigterm);
2430
2613
  try {
2431
2614
  await runWorker({
2432
- cwd: options.cwd,
2615
+ cwd: config.cwd,
2616
+ workspaceMountPoint: config.workspaceMountPoint,
2617
+ sandboxCwd: config.sandboxCwd,
2618
+ mounts: config.mounts,
2433
2619
  signal: controller.signal
2434
2620
  });
2435
2621
  if (signalExitCode !== void 0) {
@@ -2458,7 +2644,10 @@ function createProgram() {
2458
2644
  agent.action(async (prompt, options) => {
2459
2645
  await runAgent(prompt, options);
2460
2646
  });
2461
- program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <path>", "working directory").action(async (options) => {
2647
+ const sandbox = addMountOption(
2648
+ program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <host-path>", "host workspace mounted at /workspace").option("--sandbox-cwd <sandbox-path>", "initial working directory inside the sandbox")
2649
+ );
2650
+ sandbox.action(async (options) => {
2462
2651
  await runSandbox(options);
2463
2652
  });
2464
2653
  program.command("doctor").description("validate the local Ollie setup without making model calls").option("--cwd <path>", "working directory").option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option("--json", "emit JSON report").action(async (options) => {
@@ -2468,7 +2657,13 @@ function createProgram() {
2468
2657
  return program;
2469
2658
  }
2470
2659
  var RESERVED_SUBCOMMANDS = /* @__PURE__ */ new Set(["agent", "sandbox", "doctor"]);
2471
- var ROOT_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--cwd", "--timeout", "--max-output"]);
2660
+ var ROOT_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set([
2661
+ "--cwd",
2662
+ "--sandbox-cwd",
2663
+ "--mount",
2664
+ "--timeout",
2665
+ "--max-output"
2666
+ ]);
2472
2667
  function rootBoundaryPrecedesCommand(args) {
2473
2668
  for (let index = 0; index < args.length; index += 1) {
2474
2669
  const token = args[index] ?? "";