@notiamo/runner 0.1.2 → 0.2.1

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 (3) hide show
  1. package/README.md +80 -6
  2. package/dist/cli.js +1142 -263
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1045,8 +1045,8 @@ var require_safePathNames = __commonJS({
1045
1045
  })();
1046
1046
  Object.defineProperty(exports, "__esModule", { value: true });
1047
1047
  exports.isSafePathName = isSafePathName;
1048
- var path11 = __importStar(__require("path"));
1049
- function isSafePathName(name, localPath = path11) {
1048
+ var path13 = __importStar(__require("path"));
1049
+ function isSafePathName(name, localPath = path13) {
1050
1050
  if (!name || name === "." || name === ".." || name.includes("/")) {
1051
1051
  return false;
1052
1052
  }
@@ -1704,8 +1704,8 @@ var require_Client = __commonJS({
1704
1704
  /**
1705
1705
  * Set the working directory.
1706
1706
  */
1707
- async cd(path11) {
1708
- const validPath = await this.protectWhitespace(path11);
1707
+ async cd(path13) {
1708
+ const validPath = await this.protectWhitespace(path13);
1709
1709
  return this.send("CWD " + validPath);
1710
1710
  }
1711
1711
  /**
@@ -1718,8 +1718,8 @@ var require_Client = __commonJS({
1718
1718
  * Get the last modified time of a file. This is not supported by every FTP server, in which case
1719
1719
  * calling this method will throw an exception.
1720
1720
  */
1721
- async lastMod(path11) {
1722
- const validPath = await this.protectWhitespace(path11);
1721
+ async lastMod(path13) {
1722
+ const validPath = await this.protectWhitespace(path13);
1723
1723
  const res = await this.send(`MDTM ${validPath}`);
1724
1724
  const date5 = res.message.slice(4);
1725
1725
  return (0, parseListMLSD_1.parseMLSxDate)(date5);
@@ -1727,8 +1727,8 @@ var require_Client = __commonJS({
1727
1727
  /**
1728
1728
  * Get the size of a file.
1729
1729
  */
1730
- async size(path11) {
1731
- const validPath = await this.protectWhitespace(path11);
1730
+ async size(path13) {
1731
+ const validPath = await this.protectWhitespace(path13);
1732
1732
  const command = `SIZE ${validPath}`;
1733
1733
  const res = await this.send(command);
1734
1734
  const size = parseInt(res.message.slice(4), 10);
@@ -1755,8 +1755,8 @@ var require_Client = __commonJS({
1755
1755
  * You can ignore FTP error return codes which won't throw an exception if e.g.
1756
1756
  * the file doesn't exist.
1757
1757
  */
1758
- async remove(path11, ignoreErrorCodes = false) {
1759
- const validPath = await this.protectWhitespace(path11);
1758
+ async remove(path13, ignoreErrorCodes = false) {
1759
+ const validPath = await this.protectWhitespace(path13);
1760
1760
  if (ignoreErrorCodes) {
1761
1761
  return this.sendIgnoringError(`DELE ${validPath}`);
1762
1762
  }
@@ -1915,8 +1915,8 @@ var require_Client = __commonJS({
1915
1915
  *
1916
1916
  * @param [path] Path to remote file or directory.
1917
1917
  */
1918
- async list(path11 = "") {
1919
- const validPath = await this.protectWhitespace(path11);
1918
+ async list(path13 = "") {
1919
+ const validPath = await this.protectWhitespace(path13);
1920
1920
  let lastError;
1921
1921
  for (const candidate of this.availableListCommands) {
1922
1922
  const command = validPath === "" ? candidate : `${candidate} ${validPath}`;
@@ -2085,21 +2085,21 @@ var require_Client = __commonJS({
2085
2085
  /**
2086
2086
  * Remove an empty directory, will fail if not empty.
2087
2087
  */
2088
- async removeEmptyDir(path11) {
2089
- const validPath = await this.protectWhitespace(path11);
2088
+ async removeEmptyDir(path13) {
2089
+ const validPath = await this.protectWhitespace(path13);
2090
2090
  return this.send(`RMD ${validPath}`);
2091
2091
  }
2092
2092
  /**
2093
2093
  * FTP servers can't handle filenames that have leading whitespace. This method transforms
2094
2094
  * a given path to fix that issue for most cases.
2095
2095
  */
2096
- async protectWhitespace(path11) {
2097
- if (!path11.startsWith(" ")) {
2098
- return path11;
2096
+ async protectWhitespace(path13) {
2097
+ if (!path13.startsWith(" ")) {
2098
+ return path13;
2099
2099
  }
2100
2100
  const pwd = await this.pwd();
2101
2101
  const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/";
2102
- return absolutePathPrefix + path11;
2102
+ return absolutePathPrefix + path13;
2103
2103
  }
2104
2104
  async _exitAtCurrentDirectory(func) {
2105
2105
  const userDir = await this.pwd();
@@ -2176,11 +2176,11 @@ var require_Client = __commonJS({
2176
2176
  }
2177
2177
  };
2178
2178
  exports.Client = Client3;
2179
- async function ensureLocalDirectory(path11) {
2179
+ async function ensureLocalDirectory(path13) {
2180
2180
  try {
2181
- await fsStat(path11);
2181
+ await fsStat(path13);
2182
2182
  } catch (_a3) {
2183
- await fsMkDir(path11, { recursive: true });
2183
+ await fsMkDir(path13, { recursive: true });
2184
2184
  }
2185
2185
  }
2186
2186
  async function expectedBytesFromLocalFile(fd, options) {
@@ -5477,8 +5477,8 @@ var require_utils = __commonJS({
5477
5477
  }
5478
5478
  return ind;
5479
5479
  }
5480
- function removeDotSegments(path11) {
5481
- let input2 = path11;
5480
+ function removeDotSegments(path13) {
5481
+ let input2 = path13;
5482
5482
  const output2 = [];
5483
5483
  let nextSlash = -1;
5484
5484
  let len = 0;
@@ -5887,8 +5887,8 @@ var require_schemes = __commonJS({
5887
5887
  }
5888
5888
  if (wsComponent.resourceName) {
5889
5889
  const queryIndex = wsComponent.resourceName.indexOf("?");
5890
- const path11 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
5891
- wsComponent.path = path11 && path11 !== "/" ? path11 : void 0;
5890
+ const path13 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
5891
+ wsComponent.path = path13 && path13 !== "/" ? path13 : void 0;
5892
5892
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
5893
5893
  wsComponent.resourceName = void 0;
5894
5894
  }
@@ -16025,7 +16025,7 @@ var require_keyParser = __commonJS({
16025
16025
  var {
16026
16026
  createDecipheriv,
16027
16027
  createECDH,
16028
- createHash,
16028
+ createHash: createHash2,
16029
16029
  createHmac,
16030
16030
  createSign,
16031
16031
  createVerify,
@@ -16713,11 +16713,11 @@ ${formatted}-----END ${type} KEY-----`;
16713
16713
  );
16714
16714
  }
16715
16715
  const ivSlice = bufferSlice(cipherIV, 0, 8);
16716
- let cipherKey = createHash("md5").update(passphrase).update(ivSlice).digest();
16716
+ let cipherKey = createHash2("md5").update(passphrase).update(ivSlice).digest();
16717
16717
  while (cipherKey.length < encInfo.keyLen) {
16718
16718
  cipherKey = combineBuffers(
16719
16719
  cipherKey,
16720
- createHash("md5").update(cipherKey).update(passphrase).update(ivSlice).digest()
16720
+ createHash2("md5").update(cipherKey).update(passphrase).update(ivSlice).digest()
16721
16721
  );
16722
16722
  }
16723
16723
  if (cipherKey.length > encInfo.keyLen)
@@ -16882,8 +16882,8 @@ ${formatted}-----END ${type} KEY-----`;
16882
16882
  if (encrypted) {
16883
16883
  const encInfo = CIPHER_INFO[cipherName];
16884
16884
  let cipherKey = combineBuffers(
16885
- createHash("sha1").update(PPK_PP1).update(passphrase).digest(),
16886
- createHash("sha1").update(PPK_PP2).update(passphrase).digest()
16885
+ createHash2("sha1").update(PPK_PP1).update(passphrase).digest(),
16886
+ createHash2("sha1").update(PPK_PP2).update(passphrase).digest()
16887
16887
  );
16888
16888
  if (cipherKey.length > encInfo.keyLen)
16889
16889
  cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen);
@@ -16923,7 +16923,7 @@ ${formatted}-----END ${type} KEY-----`;
16923
16923
  passphrase = EMPTY_PASSPHRASE;
16924
16924
  const calcMAC = createHmac(
16925
16925
  "sha1",
16926
- createHash("sha1").update("putty-private-key-file-mac-key").update(passphrase).digest()
16926
+ createHash2("sha1").update("putty-private-key-file-mac-key").update(passphrase).digest()
16927
16927
  ).update(macData).digest("hex");
16928
16928
  if (calcMAC !== mac3) {
16929
16929
  if (encrypted) {
@@ -17567,11 +17567,11 @@ var require_agent = __commonJS({
17567
17567
  };
17568
17568
  })();
17569
17569
  var WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
17570
- function createAgent(path11) {
17571
- if (process.platform === "win32" && !WINDOWS_PIPE_REGEX.test(path11)) {
17572
- return path11 === "pageant" ? new PageantAgent() : new CygwinAgent(path11);
17570
+ function createAgent(path13) {
17571
+ if (process.platform === "win32" && !WINDOWS_PIPE_REGEX.test(path13)) {
17572
+ return path13 === "pageant" ? new PageantAgent() : new CygwinAgent(path13);
17573
17573
  }
17574
- return new OpenSSHAgent(path11);
17574
+ return new OpenSSHAgent(path13);
17575
17575
  }
17576
17576
  var AgentProtocol = (() => {
17577
17577
  const SSH_AGENTC_REQUEST_IDENTITIES = 11;
@@ -19245,7 +19245,7 @@ var require_kex = __commonJS({
19245
19245
  createDiffieHellman,
19246
19246
  createDiffieHellmanGroup,
19247
19247
  createECDH,
19248
- createHash,
19248
+ createHash: createHash2,
19249
19249
  createPublicKey,
19250
19250
  diffieHellman,
19251
19251
  generateKeyPairSync,
@@ -19671,7 +19671,7 @@ var require_kex = __commonJS({
19671
19671
  DISCONNECT_REASON.KEY_EXCHANGE_FAILED
19672
19672
  );
19673
19673
  }
19674
- const hash2 = createHash(this.hashName);
19674
+ const hash2 = createHash2(this.hashName);
19675
19675
  hashString(hash2, isServer ? this._remoteIdentRaw : this._identRaw);
19676
19676
  hashString(hash2, isServer ? this._identRaw : this._remoteIdentRaw);
19677
19677
  hashString(hash2, isServer ? this._remoteKexinit : this._kexinit);
@@ -20679,9 +20679,9 @@ var require_kex = __commonJS({
20679
20679
  function generateKEXVal(len, hashName, secret, exchangeHash, sessionID, char) {
20680
20680
  let ret;
20681
20681
  if (len) {
20682
- let digest = createHash(hashName).update(secret).update(exchangeHash).update(char).update(sessionID).digest();
20682
+ let digest = createHash2(hashName).update(secret).update(exchangeHash).update(char).update(sessionID).digest();
20683
20683
  while (digest.length < len) {
20684
- const chunk = createHash(hashName).update(secret).update(exchangeHash).update(digest).digest();
20684
+ const chunk = createHash2(hashName).update(secret).update(exchangeHash).update(digest).digest();
20685
20685
  const extended2 = Buffer.allocUnsafe(digest.length + chunk.length);
20686
20686
  extended2.set(digest, 0);
20687
20687
  extended2.set(chunk, digest.length);
@@ -22815,17 +22815,17 @@ var require_SFTP = __commonJS({
22815
22815
  // ===========================================================================
22816
22816
  // Client-specific ===========================================================
22817
22817
  // ===========================================================================
22818
- createReadStream(path11, options) {
22818
+ createReadStream(path13, options) {
22819
22819
  if (this.server)
22820
22820
  throw new Error("Client-only method called in server mode");
22821
- return new ReadStream(this, path11, options);
22821
+ return new ReadStream(this, path13, options);
22822
22822
  }
22823
- createWriteStream(path11, options) {
22823
+ createWriteStream(path13, options) {
22824
22824
  if (this.server)
22825
22825
  throw new Error("Client-only method called in server mode");
22826
- return new WriteStream(this, path11, options);
22826
+ return new WriteStream(this, path13, options);
22827
22827
  }
22828
- open(path11, flags_, attrs, cb) {
22828
+ open(path13, flags_, attrs, cb) {
22829
22829
  if (this.server)
22830
22830
  throw new Error("Client-only method called in server mode");
22831
22831
  if (typeof attrs === "function") {
@@ -22844,7 +22844,7 @@ var require_SFTP = __commonJS({
22844
22844
  attrsFlags = attrs.flags;
22845
22845
  attrsLen = attrs.nb;
22846
22846
  }
22847
- const pathLen = Buffer.byteLength(path11);
22847
+ const pathLen = Buffer.byteLength(path13);
22848
22848
  let p = 9;
22849
22849
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
22850
22850
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -22852,7 +22852,7 @@ var require_SFTP = __commonJS({
22852
22852
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
22853
22853
  writeUInt32BE(buf, reqid, 5);
22854
22854
  writeUInt32BE(buf, pathLen, p);
22855
- buf.utf8Write(path11, p += 4, pathLen);
22855
+ buf.utf8Write(path13, p += 4, pathLen);
22856
22856
  writeUInt32BE(buf, flags, p += pathLen);
22857
22857
  writeUInt32BE(buf, attrsFlags, p += 4);
22858
22858
  if (attrsLen) {
@@ -22983,7 +22983,7 @@ var require_SFTP = __commonJS({
22983
22983
  throw new Error("Client-only method called in server mode");
22984
22984
  fastXfer(fs, this, localPath, remotePath, opts, cb);
22985
22985
  }
22986
- readFile(path11, options, callback_) {
22986
+ readFile(path13, options, callback_) {
22987
22987
  if (this.server)
22988
22988
  throw new Error("Client-only method called in server mode");
22989
22989
  let callback;
@@ -23056,13 +23056,13 @@ var require_SFTP = __commonJS({
23056
23056
  return callback && callback(er, buffer);
23057
23057
  });
23058
23058
  };
23059
- this.open(path11, flag, 438, (er, handle_) => {
23059
+ this.open(path13, flag, 438, (er, handle_) => {
23060
23060
  if (er)
23061
23061
  return callback && callback(er);
23062
23062
  handle = handle_;
23063
23063
  const tryStat = (er2, st) => {
23064
23064
  if (er2) {
23065
- this.stat(path11, (er_, st_) => {
23065
+ this.stat(path13, (er_, st_) => {
23066
23066
  if (er_) {
23067
23067
  return this.close(handle, () => {
23068
23068
  callback && callback(er2);
@@ -23083,7 +23083,7 @@ var require_SFTP = __commonJS({
23083
23083
  this.fstat(handle, tryStat);
23084
23084
  });
23085
23085
  }
23086
- writeFile(path11, data, options, callback_) {
23086
+ writeFile(path13, data, options, callback_) {
23087
23087
  if (this.server)
23088
23088
  throw new Error("Client-only method called in server mode");
23089
23089
  let callback;
@@ -23102,7 +23102,7 @@ var require_SFTP = __commonJS({
23102
23102
  if (options.encoding && !Buffer.isEncoding(options.encoding))
23103
23103
  throw new Error(`Unknown encoding: ${options.encoding}`);
23104
23104
  const flag = options.flag || "w";
23105
- this.open(path11, flag, options.mode, (openErr, handle) => {
23105
+ this.open(path13, flag, options.mode, (openErr, handle) => {
23106
23106
  if (openErr) {
23107
23107
  callback && callback(openErr);
23108
23108
  } else {
@@ -23111,7 +23111,7 @@ var require_SFTP = __commonJS({
23111
23111
  if (position === null) {
23112
23112
  const tryStat = (er, st) => {
23113
23113
  if (er) {
23114
- this.stat(path11, (er_, st_) => {
23114
+ this.stat(path13, (er_, st_) => {
23115
23115
  if (er_) {
23116
23116
  return this.close(handle, () => {
23117
23117
  callback && callback(er);
@@ -23130,7 +23130,7 @@ var require_SFTP = __commonJS({
23130
23130
  }
23131
23131
  });
23132
23132
  }
23133
- appendFile(path11, data, options, callback_) {
23133
+ appendFile(path13, data, options, callback_) {
23134
23134
  if (this.server)
23135
23135
  throw new Error("Client-only method called in server mode");
23136
23136
  let callback;
@@ -23148,12 +23148,12 @@ var require_SFTP = __commonJS({
23148
23148
  throw new TypeError("Bad arguments");
23149
23149
  if (!options.flag)
23150
23150
  options = Object.assign({ flag: "a" }, options);
23151
- this.writeFile(path11, data, options, callback);
23151
+ this.writeFile(path13, data, options, callback);
23152
23152
  }
23153
- exists(path11, cb) {
23153
+ exists(path13, cb) {
23154
23154
  if (this.server)
23155
23155
  throw new Error("Client-only method called in server mode");
23156
- this.stat(path11, (err) => {
23156
+ this.stat(path13, (err) => {
23157
23157
  cb && cb(err ? false : true);
23158
23158
  });
23159
23159
  }
@@ -23196,7 +23196,7 @@ var require_SFTP = __commonJS({
23196
23196
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} RENAME`
23197
23197
  );
23198
23198
  }
23199
- mkdir(path11, attrs, cb) {
23199
+ mkdir(path13, attrs, cb) {
23200
23200
  if (this.server)
23201
23201
  throw new Error("Client-only method called in server mode");
23202
23202
  let flags = 0;
@@ -23210,7 +23210,7 @@ var require_SFTP = __commonJS({
23210
23210
  flags = attrs.flags;
23211
23211
  attrsLen = attrs.nb;
23212
23212
  }
23213
- const pathLen = Buffer.byteLength(path11);
23213
+ const pathLen = Buffer.byteLength(path13);
23214
23214
  let p = 9;
23215
23215
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
23216
23216
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23218,7 +23218,7 @@ var require_SFTP = __commonJS({
23218
23218
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23219
23219
  writeUInt32BE(buf, reqid, 5);
23220
23220
  writeUInt32BE(buf, pathLen, p);
23221
- buf.utf8Write(path11, p += 4, pathLen);
23221
+ buf.utf8Write(path13, p += 4, pathLen);
23222
23222
  writeUInt32BE(buf, flags, p += pathLen);
23223
23223
  if (attrsLen) {
23224
23224
  p += 4;
@@ -23234,10 +23234,10 @@ var require_SFTP = __commonJS({
23234
23234
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} MKDIR`
23235
23235
  );
23236
23236
  }
23237
- rmdir(path11, cb) {
23237
+ rmdir(path13, cb) {
23238
23238
  if (this.server)
23239
23239
  throw new Error("Client-only method called in server mode");
23240
- const pathLen = Buffer.byteLength(path11);
23240
+ const pathLen = Buffer.byteLength(path13);
23241
23241
  let p = 9;
23242
23242
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23243
23243
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23245,7 +23245,7 @@ var require_SFTP = __commonJS({
23245
23245
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23246
23246
  writeUInt32BE(buf, reqid, 5);
23247
23247
  writeUInt32BE(buf, pathLen, p);
23248
- buf.utf8Write(path11, p += 4, pathLen);
23248
+ buf.utf8Write(path13, p += 4, pathLen);
23249
23249
  this._requests[reqid] = { cb };
23250
23250
  const isBuffered = sendOrBuffer(this, buf);
23251
23251
  this._debug && this._debug(
@@ -23335,10 +23335,10 @@ var require_SFTP = __commonJS({
23335
23335
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} FSTAT`
23336
23336
  );
23337
23337
  }
23338
- stat(path11, cb) {
23338
+ stat(path13, cb) {
23339
23339
  if (this.server)
23340
23340
  throw new Error("Client-only method called in server mode");
23341
- const pathLen = Buffer.byteLength(path11);
23341
+ const pathLen = Buffer.byteLength(path13);
23342
23342
  let p = 9;
23343
23343
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23344
23344
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23346,17 +23346,17 @@ var require_SFTP = __commonJS({
23346
23346
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23347
23347
  writeUInt32BE(buf, reqid, 5);
23348
23348
  writeUInt32BE(buf, pathLen, p);
23349
- buf.utf8Write(path11, p += 4, pathLen);
23349
+ buf.utf8Write(path13, p += 4, pathLen);
23350
23350
  this._requests[reqid] = { cb };
23351
23351
  const isBuffered = sendOrBuffer(this, buf);
23352
23352
  this._debug && this._debug(
23353
23353
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} STAT`
23354
23354
  );
23355
23355
  }
23356
- lstat(path11, cb) {
23356
+ lstat(path13, cb) {
23357
23357
  if (this.server)
23358
23358
  throw new Error("Client-only method called in server mode");
23359
- const pathLen = Buffer.byteLength(path11);
23359
+ const pathLen = Buffer.byteLength(path13);
23360
23360
  let p = 9;
23361
23361
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23362
23362
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23364,17 +23364,17 @@ var require_SFTP = __commonJS({
23364
23364
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23365
23365
  writeUInt32BE(buf, reqid, 5);
23366
23366
  writeUInt32BE(buf, pathLen, p);
23367
- buf.utf8Write(path11, p += 4, pathLen);
23367
+ buf.utf8Write(path13, p += 4, pathLen);
23368
23368
  this._requests[reqid] = { cb };
23369
23369
  const isBuffered = sendOrBuffer(this, buf);
23370
23370
  this._debug && this._debug(
23371
23371
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} LSTAT`
23372
23372
  );
23373
23373
  }
23374
- opendir(path11, cb) {
23374
+ opendir(path13, cb) {
23375
23375
  if (this.server)
23376
23376
  throw new Error("Client-only method called in server mode");
23377
- const pathLen = Buffer.byteLength(path11);
23377
+ const pathLen = Buffer.byteLength(path13);
23378
23378
  let p = 9;
23379
23379
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23380
23380
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23382,14 +23382,14 @@ var require_SFTP = __commonJS({
23382
23382
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23383
23383
  writeUInt32BE(buf, reqid, 5);
23384
23384
  writeUInt32BE(buf, pathLen, p);
23385
- buf.utf8Write(path11, p += 4, pathLen);
23385
+ buf.utf8Write(path13, p += 4, pathLen);
23386
23386
  this._requests[reqid] = { cb };
23387
23387
  const isBuffered = sendOrBuffer(this, buf);
23388
23388
  this._debug && this._debug(
23389
23389
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} OPENDIR`
23390
23390
  );
23391
23391
  }
23392
- setstat(path11, attrs, cb) {
23392
+ setstat(path13, attrs, cb) {
23393
23393
  if (this.server)
23394
23394
  throw new Error("Client-only method called in server mode");
23395
23395
  let flags = 0;
@@ -23401,7 +23401,7 @@ var require_SFTP = __commonJS({
23401
23401
  } else if (typeof attrs === "function") {
23402
23402
  cb = attrs;
23403
23403
  }
23404
- const pathLen = Buffer.byteLength(path11);
23404
+ const pathLen = Buffer.byteLength(path13);
23405
23405
  let p = 9;
23406
23406
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
23407
23407
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23409,7 +23409,7 @@ var require_SFTP = __commonJS({
23409
23409
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23410
23410
  writeUInt32BE(buf, reqid, 5);
23411
23411
  writeUInt32BE(buf, pathLen, p);
23412
- buf.utf8Write(path11, p += 4, pathLen);
23412
+ buf.utf8Write(path13, p += 4, pathLen);
23413
23413
  writeUInt32BE(buf, flags, p += pathLen);
23414
23414
  if (attrsLen) {
23415
23415
  p += 4;
@@ -23469,8 +23469,8 @@ var require_SFTP = __commonJS({
23469
23469
  mtime: toUnixTimestamp(mtime)
23470
23470
  }, cb);
23471
23471
  }
23472
- utimes(path11, atime, mtime, cb) {
23473
- return this.setstat(path11, {
23472
+ utimes(path13, atime, mtime, cb) {
23473
+ return this.setstat(path13, {
23474
23474
  atime: toUnixTimestamp(atime),
23475
23475
  mtime: toUnixTimestamp(mtime)
23476
23476
  }, cb);
@@ -23481,8 +23481,8 @@ var require_SFTP = __commonJS({
23481
23481
  gid
23482
23482
  }, cb);
23483
23483
  }
23484
- chown(path11, uid, gid, cb) {
23485
- return this.setstat(path11, {
23484
+ chown(path13, uid, gid, cb) {
23485
+ return this.setstat(path13, {
23486
23486
  uid,
23487
23487
  gid
23488
23488
  }, cb);
@@ -23492,15 +23492,15 @@ var require_SFTP = __commonJS({
23492
23492
  mode
23493
23493
  }, cb);
23494
23494
  }
23495
- chmod(path11, mode, cb) {
23496
- return this.setstat(path11, {
23495
+ chmod(path13, mode, cb) {
23496
+ return this.setstat(path13, {
23497
23497
  mode
23498
23498
  }, cb);
23499
23499
  }
23500
- readlink(path11, cb) {
23500
+ readlink(path13, cb) {
23501
23501
  if (this.server)
23502
23502
  throw new Error("Client-only method called in server mode");
23503
- const pathLen = Buffer.byteLength(path11);
23503
+ const pathLen = Buffer.byteLength(path13);
23504
23504
  let p = 9;
23505
23505
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23506
23506
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23508,7 +23508,7 @@ var require_SFTP = __commonJS({
23508
23508
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23509
23509
  writeUInt32BE(buf, reqid, 5);
23510
23510
  writeUInt32BE(buf, pathLen, p);
23511
- buf.utf8Write(path11, p += 4, pathLen);
23511
+ buf.utf8Write(path13, p += 4, pathLen);
23512
23512
  this._requests[reqid] = {
23513
23513
  cb: (err, names) => {
23514
23514
  if (typeof cb !== "function")
@@ -23553,10 +23553,10 @@ var require_SFTP = __commonJS({
23553
23553
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} SYMLINK`
23554
23554
  );
23555
23555
  }
23556
- realpath(path11, cb) {
23556
+ realpath(path13, cb) {
23557
23557
  if (this.server)
23558
23558
  throw new Error("Client-only method called in server mode");
23559
- const pathLen = Buffer.byteLength(path11);
23559
+ const pathLen = Buffer.byteLength(path13);
23560
23560
  let p = 9;
23561
23561
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23562
23562
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23564,7 +23564,7 @@ var require_SFTP = __commonJS({
23564
23564
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23565
23565
  writeUInt32BE(buf, reqid, 5);
23566
23566
  writeUInt32BE(buf, pathLen, p);
23567
- buf.utf8Write(path11, p += 4, pathLen);
23567
+ buf.utf8Write(path13, p += 4, pathLen);
23568
23568
  this._requests[reqid] = {
23569
23569
  cb: (err, names) => {
23570
23570
  if (typeof cb !== "function")
@@ -23609,13 +23609,13 @@ var require_SFTP = __commonJS({
23609
23609
  this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`);
23610
23610
  }
23611
23611
  }
23612
- ext_openssh_statvfs(path11, cb) {
23612
+ ext_openssh_statvfs(path13, cb) {
23613
23613
  if (this.server)
23614
23614
  throw new Error("Client-only method called in server mode");
23615
23615
  const ext = this._extensions["statvfs@openssh.com"];
23616
23616
  if (!ext || ext !== "2")
23617
23617
  throw new Error("Server does not support this extended request");
23618
- const pathLen = Buffer.byteLength(path11);
23618
+ const pathLen = Buffer.byteLength(path13);
23619
23619
  let p = 9;
23620
23620
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
23621
23621
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23625,7 +23625,7 @@ var require_SFTP = __commonJS({
23625
23625
  writeUInt32BE(buf, 19, p);
23626
23626
  buf.utf8Write("statvfs@openssh.com", p += 4, 19);
23627
23627
  writeUInt32BE(buf, pathLen, p += 19);
23628
- buf.utf8Write(path11, p += 4, pathLen);
23628
+ buf.utf8Write(path13, p += 4, pathLen);
23629
23629
  this._requests[reqid] = { extended: "statvfs@openssh.com", cb };
23630
23630
  const isBuffered = sendOrBuffer(this, buf);
23631
23631
  if (this._debug) {
@@ -23711,7 +23711,7 @@ var require_SFTP = __commonJS({
23711
23711
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} fsync@openssh.com`
23712
23712
  );
23713
23713
  }
23714
- ext_openssh_lsetstat(path11, attrs, cb) {
23714
+ ext_openssh_lsetstat(path13, attrs, cb) {
23715
23715
  if (this.server)
23716
23716
  throw new Error("Client-only method called in server mode");
23717
23717
  const ext = this._extensions["lsetstat@openssh.com"];
@@ -23726,7 +23726,7 @@ var require_SFTP = __commonJS({
23726
23726
  } else if (typeof attrs === "function") {
23727
23727
  cb = attrs;
23728
23728
  }
23729
- const pathLen = Buffer.byteLength(path11);
23729
+ const pathLen = Buffer.byteLength(path13);
23730
23730
  let p = 9;
23731
23731
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
23732
23732
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23736,7 +23736,7 @@ var require_SFTP = __commonJS({
23736
23736
  writeUInt32BE(buf, 20, p);
23737
23737
  buf.utf8Write("lsetstat@openssh.com", p += 4, 20);
23738
23738
  writeUInt32BE(buf, pathLen, p += 20);
23739
- buf.utf8Write(path11, p += 4, pathLen);
23739
+ buf.utf8Write(path13, p += 4, pathLen);
23740
23740
  writeUInt32BE(buf, flags, p += pathLen);
23741
23741
  if (attrsLen) {
23742
23742
  p += 4;
@@ -23753,13 +23753,13 @@ var require_SFTP = __commonJS({
23753
23753
  this._debug(`SFTP: Outbound: ${status5} lsetstat@openssh.com`);
23754
23754
  }
23755
23755
  }
23756
- ext_openssh_expandPath(path11, cb) {
23756
+ ext_openssh_expandPath(path13, cb) {
23757
23757
  if (this.server)
23758
23758
  throw new Error("Client-only method called in server mode");
23759
23759
  const ext = this._extensions["expand-path@openssh.com"];
23760
23760
  if (ext !== "1")
23761
23761
  throw new Error("Server does not support this extended request");
23762
- const pathLen = Buffer.byteLength(path11);
23762
+ const pathLen = Buffer.byteLength(path13);
23763
23763
  let p = 9;
23764
23764
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
23765
23765
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23769,7 +23769,7 @@ var require_SFTP = __commonJS({
23769
23769
  writeUInt32BE(buf, 23, p);
23770
23770
  buf.utf8Write("expand-path@openssh.com", p += 4, 23);
23771
23771
  writeUInt32BE(buf, pathLen, p += 20);
23772
- buf.utf8Write(path11, p += 4, pathLen);
23772
+ buf.utf8Write(path13, p += 4, pathLen);
23773
23773
  this._requests[reqid] = {
23774
23774
  cb: (err, names) => {
23775
23775
  if (typeof cb !== "function")
@@ -25047,12 +25047,12 @@ var require_SFTP = __commonJS({
25047
25047
  [REQUEST.LSTAT]: (sftp, payload) => {
25048
25048
  bufferParser.init(payload, 1);
25049
25049
  const reqID = bufferParser.readUInt32BE();
25050
- const path11 = bufferParser.readString(true);
25050
+ const path13 = bufferParser.readString(true);
25051
25051
  bufferParser.clear();
25052
- if (path11 === void 0)
25052
+ if (path13 === void 0)
25053
25053
  return doFatalSFTPError(sftp, "Malformed LSTAT packet");
25054
25054
  sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`);
25055
- if (!sftp.emit("LSTAT", reqID, path11)) {
25055
+ if (!sftp.emit("LSTAT", reqID, path13)) {
25056
25056
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25057
25057
  }
25058
25058
  },
@@ -25071,13 +25071,13 @@ var require_SFTP = __commonJS({
25071
25071
  [REQUEST.SETSTAT]: (sftp, payload) => {
25072
25072
  bufferParser.init(payload, 1);
25073
25073
  const reqID = bufferParser.readUInt32BE();
25074
- const path11 = bufferParser.readString(true);
25074
+ const path13 = bufferParser.readString(true);
25075
25075
  const attrs = readAttrs(sftp._biOpt);
25076
25076
  bufferParser.clear();
25077
25077
  if (attrs === void 0)
25078
25078
  return doFatalSFTPError(sftp, "Malformed SETSTAT packet");
25079
25079
  sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`);
25080
- if (!sftp.emit("SETSTAT", reqID, path11, attrs)) {
25080
+ if (!sftp.emit("SETSTAT", reqID, path13, attrs)) {
25081
25081
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25082
25082
  }
25083
25083
  },
@@ -25099,12 +25099,12 @@ var require_SFTP = __commonJS({
25099
25099
  [REQUEST.OPENDIR]: (sftp, payload) => {
25100
25100
  bufferParser.init(payload, 1);
25101
25101
  const reqID = bufferParser.readUInt32BE();
25102
- const path11 = bufferParser.readString(true);
25102
+ const path13 = bufferParser.readString(true);
25103
25103
  bufferParser.clear();
25104
- if (path11 === void 0)
25104
+ if (path13 === void 0)
25105
25105
  return doFatalSFTPError(sftp, "Malformed OPENDIR packet");
25106
25106
  sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`);
25107
- if (!sftp.emit("OPENDIR", reqID, path11)) {
25107
+ if (!sftp.emit("OPENDIR", reqID, path13)) {
25108
25108
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25109
25109
  }
25110
25110
  },
@@ -25123,63 +25123,63 @@ var require_SFTP = __commonJS({
25123
25123
  [REQUEST.REMOVE]: (sftp, payload) => {
25124
25124
  bufferParser.init(payload, 1);
25125
25125
  const reqID = bufferParser.readUInt32BE();
25126
- const path11 = bufferParser.readString(true);
25126
+ const path13 = bufferParser.readString(true);
25127
25127
  bufferParser.clear();
25128
- if (path11 === void 0)
25128
+ if (path13 === void 0)
25129
25129
  return doFatalSFTPError(sftp, "Malformed REMOVE packet");
25130
25130
  sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`);
25131
- if (!sftp.emit("REMOVE", reqID, path11)) {
25131
+ if (!sftp.emit("REMOVE", reqID, path13)) {
25132
25132
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25133
25133
  }
25134
25134
  },
25135
25135
  [REQUEST.MKDIR]: (sftp, payload) => {
25136
25136
  bufferParser.init(payload, 1);
25137
25137
  const reqID = bufferParser.readUInt32BE();
25138
- const path11 = bufferParser.readString(true);
25138
+ const path13 = bufferParser.readString(true);
25139
25139
  const attrs = readAttrs(sftp._biOpt);
25140
25140
  bufferParser.clear();
25141
25141
  if (attrs === void 0)
25142
25142
  return doFatalSFTPError(sftp, "Malformed MKDIR packet");
25143
25143
  sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`);
25144
- if (!sftp.emit("MKDIR", reqID, path11, attrs)) {
25144
+ if (!sftp.emit("MKDIR", reqID, path13, attrs)) {
25145
25145
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25146
25146
  }
25147
25147
  },
25148
25148
  [REQUEST.RMDIR]: (sftp, payload) => {
25149
25149
  bufferParser.init(payload, 1);
25150
25150
  const reqID = bufferParser.readUInt32BE();
25151
- const path11 = bufferParser.readString(true);
25151
+ const path13 = bufferParser.readString(true);
25152
25152
  bufferParser.clear();
25153
- if (path11 === void 0)
25153
+ if (path13 === void 0)
25154
25154
  return doFatalSFTPError(sftp, "Malformed RMDIR packet");
25155
25155
  sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`);
25156
- if (!sftp.emit("RMDIR", reqID, path11)) {
25156
+ if (!sftp.emit("RMDIR", reqID, path13)) {
25157
25157
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25158
25158
  }
25159
25159
  },
25160
25160
  [REQUEST.REALPATH]: (sftp, payload) => {
25161
25161
  bufferParser.init(payload, 1);
25162
25162
  const reqID = bufferParser.readUInt32BE();
25163
- const path11 = bufferParser.readString(true);
25163
+ const path13 = bufferParser.readString(true);
25164
25164
  bufferParser.clear();
25165
- if (path11 === void 0)
25165
+ if (path13 === void 0)
25166
25166
  return doFatalSFTPError(sftp, "Malformed REALPATH packet");
25167
25167
  sftp._debug && sftp._debug(
25168
25168
  `SFTP: Inbound: Received REALPATH (id:${reqID})`
25169
25169
  );
25170
- if (!sftp.emit("REALPATH", reqID, path11)) {
25170
+ if (!sftp.emit("REALPATH", reqID, path13)) {
25171
25171
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25172
25172
  }
25173
25173
  },
25174
25174
  [REQUEST.STAT]: (sftp, payload) => {
25175
25175
  bufferParser.init(payload, 1);
25176
25176
  const reqID = bufferParser.readUInt32BE();
25177
- const path11 = bufferParser.readString(true);
25177
+ const path13 = bufferParser.readString(true);
25178
25178
  bufferParser.clear();
25179
- if (path11 === void 0)
25179
+ if (path13 === void 0)
25180
25180
  return doFatalSFTPError(sftp, "Malformed STAT packet");
25181
25181
  sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`);
25182
- if (!sftp.emit("STAT", reqID, path11)) {
25182
+ if (!sftp.emit("STAT", reqID, path13)) {
25183
25183
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25184
25184
  }
25185
25185
  },
@@ -25199,14 +25199,14 @@ var require_SFTP = __commonJS({
25199
25199
  [REQUEST.READLINK]: (sftp, payload) => {
25200
25200
  bufferParser.init(payload, 1);
25201
25201
  const reqID = bufferParser.readUInt32BE();
25202
- const path11 = bufferParser.readString(true);
25202
+ const path13 = bufferParser.readString(true);
25203
25203
  bufferParser.clear();
25204
- if (path11 === void 0)
25204
+ if (path13 === void 0)
25205
25205
  return doFatalSFTPError(sftp, "Malformed READLINK packet");
25206
25206
  sftp._debug && sftp._debug(
25207
25207
  `SFTP: Inbound: Received READLINK (id:${reqID})`
25208
25208
  );
25209
- if (!sftp.emit("READLINK", reqID, path11)) {
25209
+ if (!sftp.emit("READLINK", reqID, path13)) {
25210
25210
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25211
25211
  }
25212
25212
  },
@@ -25277,7 +25277,7 @@ var require_SFTP = __commonJS({
25277
25277
  function roundUpToMultipleOf8(n) {
25278
25278
  return n + 7 & ~7;
25279
25279
  }
25280
- function ReadStream(sftp, path11, options) {
25280
+ function ReadStream(sftp, path13, options) {
25281
25281
  if (options === void 0)
25282
25282
  options = {};
25283
25283
  else if (typeof options === "string")
@@ -25291,7 +25291,7 @@ var require_SFTP = __commonJS({
25291
25291
  options.emitClose = false;
25292
25292
  options.autoDestroy = false;
25293
25293
  ReadableStream2.call(this, options);
25294
- this.path = path11;
25294
+ this.path = path13;
25295
25295
  this.flags = options.flags === void 0 ? "r" : options.flags;
25296
25296
  this.mode = options.mode === void 0 ? 438 : options.mode;
25297
25297
  this.start = options.start;
@@ -25422,7 +25422,7 @@ var require_SFTP = __commonJS({
25422
25422
  },
25423
25423
  configurable: true
25424
25424
  });
25425
- function WriteStream(sftp, path11, options) {
25425
+ function WriteStream(sftp, path13, options) {
25426
25426
  if (options === void 0)
25427
25427
  options = {};
25428
25428
  else if (typeof options === "string")
@@ -25434,7 +25434,7 @@ var require_SFTP = __commonJS({
25434
25434
  options.emitClose = false;
25435
25435
  options.autoDestroy = false;
25436
25436
  WritableStream.call(this, options);
25437
- this.path = path11;
25437
+ this.path = path13;
25438
25438
  this.flags = options.flags === void 0 ? "w" : options.flags;
25439
25439
  this.mode = options.mode === void 0 ? 438 : options.mode;
25440
25440
  this.start = options.start;
@@ -26116,7 +26116,7 @@ var require_client = __commonJS({
26116
26116
  "../../node_modules/.pnpm/ssh2@1.17.0/node_modules/ssh2/lib/client.js"(exports, module) {
26117
26117
  "use strict";
26118
26118
  var {
26119
- createHash,
26119
+ createHash: createHash2,
26120
26120
  getHashes,
26121
26121
  randomFillSync
26122
26122
  } = __require("crypto");
@@ -26338,7 +26338,7 @@ var require_client = __commonJS({
26338
26338
  }
26339
26339
  hostVerifier = (key, verify) => {
26340
26340
  if (hashAlgo)
26341
- key = createHash(hashAlgo).update(key).digest("hex");
26341
+ key = createHash2(hashAlgo).update(key).digest("hex");
26342
26342
  const ret = hashCb(key, verify);
26343
26343
  if (ret !== void 0)
26344
26344
  verify(ret);
@@ -29655,7 +29655,11 @@ function paths(dir) {
29655
29655
  log: path.join(dir, "runner.log"),
29656
29656
  claudeConfigDir: path.join(dir, "claude"),
29657
29657
  projectsDir: path.join(dir, "projects"),
29658
- stateFile: path.join(dir, "state.json")
29658
+ stateFile: path.join(dir, "state.json"),
29659
+ /** Each run's Claude Code settings file, `<runId>.json`, there only while the run lasts. */
29660
+ runsDir: path.join(dir, "runs"),
29661
+ /** The runner's bare mirrors of the projects' repositories: the only ones that talk to GitHub. */
29662
+ gitDir: path.join(dir, "git")
29659
29663
  };
29660
29664
  }
29661
29665
  function loadConfig(dir = defaultDir()) {
@@ -29693,8 +29697,8 @@ function parseArgs(argv) {
29693
29697
  }
29694
29698
 
29695
29699
  // src/commands.ts
29696
- import { readFileSync as readFileSync3 } from "node:fs";
29697
- import os3 from "node:os";
29700
+ import { readFileSync as readFileSync4 } from "node:fs";
29701
+ import os6 from "node:os";
29698
29702
  import { createInterface } from "node:readline/promises";
29699
29703
  import { Writable as Writable2 } from "node:stream";
29700
29704
 
@@ -29736,6 +29740,10 @@ var NotiamoApi = class {
29736
29740
  done(runId, body) {
29737
29741
  return this.json("POST", `/api/runner/runs/${encodeURIComponent(runId)}/done`, body);
29738
29742
  }
29743
+ /** A fresh GitHub token for the run's repository, when the spec's one is close to its end. */
29744
+ gitToken(runId) {
29745
+ return this.json("POST", `/api/runner/runs/${encodeURIComponent(runId)}/git-token`, {});
29746
+ }
29739
29747
  /** Uploads a gzip backup of what the run changed. */
29740
29748
  async backup(runId, gzip) {
29741
29749
  await this.call("POST", `/api/runner/runs/${encodeURIComponent(runId)}/backup`, gzip, "application/gzip");
@@ -29758,10 +29766,91 @@ var NotiamoApi = class {
29758
29766
 
29759
29767
  // src/engine/claude.ts
29760
29768
  import * as child_process from "node:child_process";
29769
+ import os2 from "node:os";
29761
29770
  var MAX_TEXT = 2e3;
29762
29771
  var KILL_GRACE_MS = 1e4;
29763
29772
  var CLOSE_GRACE_MS = 2e3;
29764
29773
  var cut = (s) => s.length > MAX_TEXT ? s.slice(0, MAX_TEXT) : s;
29774
+ var ENV_ALLOW = [
29775
+ "PATH",
29776
+ "HOME",
29777
+ "USER",
29778
+ "LOGNAME",
29779
+ "SHELL",
29780
+ "LANG",
29781
+ "LC_ALL",
29782
+ "LC_CTYPE",
29783
+ "TERM",
29784
+ "TMPDIR",
29785
+ "TZ",
29786
+ // A machine behind a proxy or with its own CA needs these to reach anything.
29787
+ "HTTP_PROXY",
29788
+ "HTTPS_PROXY",
29789
+ "NO_PROXY",
29790
+ "http_proxy",
29791
+ "https_proxy",
29792
+ "no_proxy",
29793
+ "NODE_EXTRA_CA_CERTS",
29794
+ "SSL_CERT_FILE",
29795
+ "SSL_CERT_DIR"
29796
+ ];
29797
+ var ENV_ALLOW_PREFIX = ["XDG_"];
29798
+ var ENV_ALLOW_WINDOWS = [
29799
+ "PATHEXT",
29800
+ "SYSTEMROOT",
29801
+ "SYSTEMDRIVE",
29802
+ "WINDIR",
29803
+ "COMSPEC",
29804
+ "USERPROFILE",
29805
+ "USERNAME",
29806
+ "HOMEDRIVE",
29807
+ "HOMEPATH",
29808
+ "APPDATA",
29809
+ "LOCALAPPDATA",
29810
+ "PROGRAMDATA",
29811
+ "PROGRAMFILES",
29812
+ "PROGRAMFILES(X86)",
29813
+ "TEMP",
29814
+ "TMP"
29815
+ ];
29816
+ function baseEnv(env, windows) {
29817
+ const out = {};
29818
+ for (const [k, v] of Object.entries(env)) {
29819
+ if (v === void 0) continue;
29820
+ const key = windows ? k.toUpperCase() : k;
29821
+ if (!(ENV_ALLOW.includes(key) || ENV_ALLOW_PREFIX.some((p) => key.startsWith(p)) || windows && ENV_ALLOW_WINDOWS.includes(key))) continue;
29822
+ if (PROXY_URLS.includes(key.toUpperCase())) {
29823
+ const url2 = withoutUserinfo(v);
29824
+ if (url2 !== null) out[k] = url2;
29825
+ continue;
29826
+ }
29827
+ out[k] = v;
29828
+ }
29829
+ return out;
29830
+ }
29831
+ var PROXY_URLS = ["HTTP_PROXY", "HTTPS_PROXY"];
29832
+ function withoutUserinfo(value) {
29833
+ try {
29834
+ const u = new URL(value);
29835
+ u.username = "";
29836
+ u.password = "";
29837
+ return u.toString();
29838
+ } catch {
29839
+ return null;
29840
+ }
29841
+ }
29842
+ var AGENT_GIT_ENV = {
29843
+ GIT_CONFIG_GLOBAL: os2.devNull,
29844
+ GIT_CONFIG_NOSYSTEM: "1",
29845
+ GIT_TERMINAL_PROMPT: "0",
29846
+ GIT_AUTHOR_NAME: "Notiamo agent",
29847
+ GIT_AUTHOR_EMAIL: "agent@notiamo.com",
29848
+ GIT_COMMITTER_NAME: "Notiamo agent",
29849
+ GIT_COMMITTER_EMAIL: "agent@notiamo.com"
29850
+ };
29851
+ function isSandboxUnavailable(line) {
29852
+ return /sandbox/i.test(line) && /(unavailable|not available|stopped being available|failed to (start|initiali[sz]e)|requires wsl2|bubblewrap|bwrap|socat)/i.test(line);
29853
+ }
29765
29854
  function claudeEngine(deps = {}) {
29766
29855
  const spawn4 = deps.spawn ?? child_process.spawn;
29767
29856
  const bin = deps.claudeBin ?? "claude";
@@ -29782,13 +29871,22 @@ function claudeEngine(deps = {}) {
29782
29871
  // tokens never show on the command line (`ps`). --mcp-config is variadic: the next argument must be a --flag.
29783
29872
  "--mcp-config",
29784
29873
  JSON.stringify(mcpConfig(o)),
29785
- // Only Notiamo's servers: permissions are skipped, so no .mcp.json of the cloned repo or of the config dir may load.
29874
+ // Only Notiamo's servers: no .mcp.json of the cloned repo or of the config dir may load.
29786
29875
  "--strict-mcp-config",
29787
- "--dangerously-skip-permissions"
29876
+ // Edits are accepted only inside the working folder; the settings file allows every (sandboxed) Bash command and
29877
+ // Notiamo's tools. Anything else would prompt, and in -p mode a prompt is a refusal.
29878
+ "--permission-mode",
29879
+ "acceptEdits",
29880
+ // The run's settings file (allow and deny rules, read and write block, shell sandbox): only paths and domains, never a secret.
29881
+ ...o.settingsFile ? ["--settings", o.settingsFile] : [],
29882
+ // Only the runner's own user settings (in CLAUDE_CONFIG_DIR): the clone's .claude/settings*.json never load.
29883
+ "--setting-sources",
29884
+ "user"
29788
29885
  ];
29789
29886
  const engineEnv = (o) => ({
29790
- ...process.env,
29887
+ ...baseEnv(deps.env ?? process.env, windows),
29791
29888
  ...o.env,
29889
+ ...AGENT_GIT_ENV,
29792
29890
  CLAUDE_CONFIG_DIR: o.claudeConfigDir,
29793
29891
  NOTIAMO_RUN_TOKEN: o.mcp.token,
29794
29892
  ...o.helper ? { NOTIAMO_WP_TOKEN: o.helper.token } : {}
@@ -29799,6 +29897,7 @@ function claudeEngine(deps = {}) {
29799
29897
  reported: false,
29800
29898
  result: false,
29801
29899
  resultError: void 0,
29900
+ sandboxError: false,
29802
29901
  limitAt: void 0,
29803
29902
  timedOut: false,
29804
29903
  spawnError: void 0,
@@ -29881,7 +29980,9 @@ function claudeEngine(deps = {}) {
29881
29980
  }
29882
29981
  lines(child.stdout, onLine);
29883
29982
  lines(child.stderr, (l) => {
29884
- if (l.trim()) onText("error", l, true);
29983
+ if (!l.trim()) return;
29984
+ if (isSandboxUnavailable(l)) st.sandboxError = true;
29985
+ onText("error", l, true);
29885
29986
  });
29886
29987
  for (const s of [child.stdout, child.stderr]) s?.on("error", (e) => emit({ kind: "error", text: cut(`stream error: ${e.message}`) }));
29887
29988
  const kill = (sig) => {
@@ -29908,6 +30009,12 @@ function claudeEngine(deps = {}) {
29908
30009
  clearTimeout(timer);
29909
30010
  clearTimeout(killTimer);
29910
30011
  clearTimeout(closeTimer);
30012
+ if (!windows && child.pid) {
30013
+ try {
30014
+ processKill(-child.pid, "SIGKILL");
30015
+ } catch {
30016
+ }
30017
+ }
29911
30018
  const usage = { costUsd: st.costUsd, inputTokens: st.inputTokens, outputTokens: st.outputTokens, sessionId: st.sessionId };
29912
30019
  const succeeded = st.result && !st.resultError;
29913
30020
  if (st.timedOut) return resolve({ outcome: "failed", error: "timeout", ...usage });
@@ -29916,6 +30023,7 @@ function claudeEngine(deps = {}) {
29916
30023
  if (st.resultError) return resolve({ outcome: "failed", error: st.resultError, ...usage });
29917
30024
  if (st.reported) return resolve({ outcome: "reported", ...usage });
29918
30025
  if (st.result) return resolve({ outcome: "ended", ...usage });
30026
+ if (code !== 0 && st.sandboxError) return resolve({ outcome: "failed", error: "sandbox_unavailable", ...usage });
29919
30027
  if (code !== 0) return resolve({ outcome: "failed", error: `claude exited with code ${code}`, ...usage });
29920
30028
  resolve({ outcome: "ended", ...usage });
29921
30029
  };
@@ -30044,15 +30152,15 @@ async function reportRun(mcp, args, fetchImpl) {
30044
30152
  // src/exec.ts
30045
30153
  import { spawn as spawn2 } from "node:child_process";
30046
30154
  var realExec = async (file2, args, opts) => {
30047
- const r = await run(file2, args, opts?.env, false);
30048
- if (r.code === 127 && process.platform === "win32") return run(file2, args.map(quoteForCmd), opts?.env, true);
30155
+ const r = await run(file2, args, opts?.env, false, opts?.timeout);
30156
+ if (r.code === 127 && process.platform === "win32") return run(file2, args.map(quoteForCmd), opts?.env, true, opts?.timeout);
30049
30157
  return r;
30050
30158
  };
30051
- function run(file2, args, env, shell) {
30159
+ function run(file2, args, env, shell, timeout) {
30052
30160
  return new Promise((resolve) => {
30053
30161
  let stdout = "";
30054
30162
  let stderr = "";
30055
- const child = spawn2(file2, args, { env: env ?? process.env, shell, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
30163
+ const child = spawn2(file2, args, { env: env ?? process.env, shell, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...timeout ? { timeout, killSignal: "SIGKILL" } : {} });
30056
30164
  child.stdout.on("data", (d) => {
30057
30165
  stdout += d.toString();
30058
30166
  });
@@ -30067,7 +30175,259 @@ var quoteForCmd = (a) => /^[\w.:/\\=-]+$/.test(a) ? a : `"${a.replace(/"/g, '""'
30067
30175
 
30068
30176
  // src/init.ts
30069
30177
  import { spawn as spawn3 } from "node:child_process";
30070
- import os2 from "node:os";
30178
+ import os4 from "node:os";
30179
+
30180
+ // src/sandbox.ts
30181
+ import { accessSync, constants as fsConstants, existsSync, readdirSync, realpathSync } from "node:fs";
30182
+ import os3 from "node:os";
30183
+ import path2 from "node:path";
30184
+ var ENGINE_MIN_VERSION = "2.1.314";
30185
+ var DEFAULT_DOMAINS = ["github.com", "api.github.com", "codeload.github.com", "objects.githubusercontent.com", "registry.npmjs.org"];
30186
+ function versionAtLeast(version2, min) {
30187
+ const parse3 = (v) => /(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number);
30188
+ const a = version2 ? parse3(version2) : void 0;
30189
+ const b = parse3(min);
30190
+ if (!a || !b) return false;
30191
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
30192
+ return true;
30193
+ }
30194
+ function engineTooOld(version2) {
30195
+ return !versionAtLeast(version2, ENGINE_MIN_VERSION);
30196
+ }
30197
+ var CREDENTIALS = [
30198
+ ".ssh",
30199
+ ".aws",
30200
+ ".gnupg",
30201
+ ".netrc",
30202
+ ".docker",
30203
+ path2.join(".config", "gh"),
30204
+ ".git-credentials",
30205
+ path2.join(".config", "git"),
30206
+ ".npmrc",
30207
+ ".pypirc",
30208
+ path2.join(".config", "gcloud"),
30209
+ path2.join(".config", "hub")
30210
+ ];
30211
+ var BASH_DENY = [
30212
+ "Bash(security *)",
30213
+ "Bash(git credential*)",
30214
+ "Bash(git-credential*)",
30215
+ "Bash(gh auth*)",
30216
+ "Bash(ssh-add *)",
30217
+ // A `*` may stand anywhere in a Bash rule: these also catch `git -c x=y credential fill`, a helper by its full path, `sh -c '…'`.
30218
+ "Bash(*credential*)",
30219
+ "Bash(*osxkeychain*)",
30220
+ "Bash(*libsecret*)",
30221
+ "Bash(*security find-*)",
30222
+ "Bash(*secret-tool*)",
30223
+ "Bash(*gh auth*)"
30224
+ ];
30225
+ var GIT_HELPERS = [
30226
+ "git-credential-osxkeychain",
30227
+ "git-credential-libsecret",
30228
+ "git-credential-store",
30229
+ "git-credential-cache",
30230
+ "git-credential-manager",
30231
+ "git-credential-manager-core"
30232
+ ];
30233
+ var HELPER_DIRS = [
30234
+ "/Library/Developer/CommandLineTools/usr/libexec/git-core",
30235
+ "/Applications/Xcode.app/Contents/Developer/usr/libexec/git-core",
30236
+ "/usr/libexec/git-core",
30237
+ "/usr/lib/git-core",
30238
+ "/usr/local/share/gcm-core",
30239
+ "/opt/homebrew/share/gcm-core",
30240
+ "/usr/local/bin"
30241
+ ];
30242
+ var KEYCHAIN_TOOLS = {
30243
+ darwin: ["/usr/bin/security", "/usr/bin/osascript"],
30244
+ linux: ["/usr/bin/secret-tool", "/usr/bin/gnome-keyring"]
30245
+ };
30246
+ async function credentialTools(exec, platform = process.platform) {
30247
+ const first = (out) => out.split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? null;
30248
+ const git = await exec("git", ["--exec-path"]).catch(() => null);
30249
+ const gh = await exec(platform === "win32" ? "where" : "which", ["gh"]).catch(() => null);
30250
+ const ghFound = gh?.code === 0 ? first(gh.stdout) : null;
30251
+ return {
30252
+ gitExecPath: git?.code === 0 ? first(git.stdout) : null,
30253
+ ghPath: ghFound && path2.isAbsolute(ghFound) ? realPath(ghFound) : null
30254
+ };
30255
+ }
30256
+ function findOnPath(name, dirs, platform) {
30257
+ const names = platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name];
30258
+ for (const d of dirs) {
30259
+ for (const n of names) {
30260
+ const file2 = path2.join(d, n);
30261
+ try {
30262
+ accessSync(file2, fsConstants.X_OK);
30263
+ return realpathSync(file2);
30264
+ } catch {
30265
+ }
30266
+ }
30267
+ }
30268
+ return null;
30269
+ }
30270
+ function machineToolDirs(env = process.env, argv1 = process.argv[1], execPath = process.execPath, platform = process.platform) {
30271
+ const pathDirs = (env.PATH ?? env.Path ?? "").split(path2.delimiter).filter((d) => d && path2.isAbsolute(d));
30272
+ const dirs = pathDirs.map(realPath);
30273
+ for (const bin of ["git", "claude"]) {
30274
+ const found = findOnPath(bin, pathDirs, platform);
30275
+ if (found) dirs.push(path2.dirname(found));
30276
+ }
30277
+ dirs.push(path2.dirname(realPath(execPath)));
30278
+ if (argv1) {
30279
+ const install4 = path2.dirname(realPath(argv1));
30280
+ dirs.push(install4);
30281
+ if (path2.basename(install4) === "dist") dirs.push(path2.dirname(install4));
30282
+ }
30283
+ return [...new Set(dirs)];
30284
+ }
30285
+ function realListEntries(dir) {
30286
+ try {
30287
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isFile() || e.isSymbolicLink()).map((e) => ({ name: e.name, dir: e.isDirectory() }));
30288
+ } catch {
30289
+ return [];
30290
+ }
30291
+ }
30292
+ function realPath(p) {
30293
+ const abs = path2.resolve(p);
30294
+ const rest = [];
30295
+ for (let cur = abs; ; ) {
30296
+ try {
30297
+ return path2.join(realpathSync(cur), ...rest);
30298
+ } catch {
30299
+ const parent = path2.dirname(cur);
30300
+ if (parent === cur) return abs;
30301
+ rest.unshift(path2.basename(cur));
30302
+ cur = parent;
30303
+ }
30304
+ }
30305
+ }
30306
+ function rulePath(p) {
30307
+ const posix = p.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_m, d) => `/${d.toLowerCase()}`);
30308
+ return `/${posix}`;
30309
+ }
30310
+ function credentialBinaries(o, toolDirs, listEntries, exists2) {
30311
+ const out = [];
30312
+ const gitReal = /* @__PURE__ */ new Set();
30313
+ const scan = [];
30314
+ const execDir = o.gitExecPath && path2.isAbsolute(o.gitExecPath) ? realPath(o.gitExecPath) : null;
30315
+ for (const d of /* @__PURE__ */ new Set([...execDir ? [execDir] : [], ...toolDirs, ...HELPER_DIRS.map(realPath)])) {
30316
+ const names = listEntries(d).map((e) => e.name);
30317
+ if (names.includes("git")) gitReal.add(realPath(path2.join(d, "git")));
30318
+ const helpers = names.filter((n) => n.startsWith("git-credential-"));
30319
+ scan.push({ dir: d, names: d === execDir ? [.../* @__PURE__ */ new Set([...helpers, ...GIT_HELPERS])] : helpers });
30320
+ }
30321
+ for (const { dir, names } of scan) {
30322
+ for (const n of names) {
30323
+ const own2 = path2.join(dir, n);
30324
+ const real = realPath(own2);
30325
+ out.push(gitReal.has(real) ? own2 : real);
30326
+ }
30327
+ }
30328
+ for (const t of KEYCHAIN_TOOLS[o.platform] ?? []) if (exists2(t)) out.push(realPath(t));
30329
+ if (o.ghPath && path2.isAbsolute(o.ghPath) && !gitReal.has(realPath(o.ghPath))) out.push(realPath(o.ghPath));
30330
+ return out;
30331
+ }
30332
+ function sandboxSettings(o) {
30333
+ const root = realPath(o.runnerRoot);
30334
+ const claudeDir = realPath(o.claudeConfigDir);
30335
+ const runDir = realPath(o.runDir);
30336
+ const home = realPath(o.home ?? os3.homedir());
30337
+ const at = (...parts) => path2.join(...parts);
30338
+ const edit = (p) => `Edit(${rulePath(p)})`;
30339
+ const gitHooks = at(runDir, ".git", "hooks");
30340
+ const gitConfig = at(runDir, ".git", "config");
30341
+ const listEntries = o.listEntries ?? realListEntries;
30342
+ const exists2 = o.exists ?? existsSync;
30343
+ const walked = [];
30344
+ const visited = /* @__PURE__ */ new Set();
30345
+ const holds = (dir, p) => p === dir || p.startsWith(dir.endsWith(path2.sep) ? dir : dir + path2.sep);
30346
+ const denyAllBut = (dir) => {
30347
+ if (visited.has(dir)) return;
30348
+ visited.add(dir);
30349
+ for (const e of listEntries(dir)) {
30350
+ const child = realPath(at(dir, e.name));
30351
+ if (child === runDir) continue;
30352
+ if (holds(child, runDir)) {
30353
+ if (holds(root, child) && child !== root) denyAllBut(child);
30354
+ } else if (child !== path2.parse(child).root) walked.push({ name: child, dir: e.dir });
30355
+ }
30356
+ };
30357
+ denyAllBut(root);
30358
+ const profiles = listEntries(at(root, "profiles")).filter((e) => e.dir).map((e) => at(root, "profiles", e.name));
30359
+ const known = [root, ...profiles].flatMap((d) => [realPath(at(d, "config.json")), realPath(at(d, "state.json"))]);
30360
+ const toolDirs = (o.toolDirs ?? machineToolDirs()).map(realPath);
30361
+ const denyRead = [...walked.map((e) => e.name), ...known];
30362
+ if (exists2(claudeDir)) denyRead.push(claudeDir);
30363
+ for (const c of CREDENTIALS) if (exists2(at(home, c))) denyRead.push(realPath(at(home, c)));
30364
+ denyRead.push(...credentialBinaries(o, toolDirs, listEntries, exists2));
30365
+ const holdsRun = (dir) => holds(dir, runDir);
30366
+ const editTree = (dir) => holdsRun(dir) ? [] : [edit(`${dir}/**`)];
30367
+ const deny = [
30368
+ edit(at(root, "config.json")),
30369
+ edit(at(root, "state.json")),
30370
+ edit(at(root, "runner.log")),
30371
+ ...editTree(at(root, "runs")),
30372
+ edit(at(root, "profiles", "*", "config.json")),
30373
+ edit(at(root, "profiles", "*", "state.json")),
30374
+ edit(`${at(root, "profiles", "*", "runs")}/**`),
30375
+ ...editTree(claudeDir),
30376
+ edit(`${at(root, "profiles", "*", "claude")}/**`),
30377
+ ...[".ssh", ".aws", ".gnupg", ".docker", ".config", ".npm", ".local", ".claude", ...o.platform === "darwin" ? ["Library"] : []].flatMap((d) => editTree(at(home, d))),
30378
+ ...[
30379
+ ".netrc",
30380
+ ".git-credentials",
30381
+ ".npmrc",
30382
+ ".pypirc",
30383
+ ".zshrc",
30384
+ ".zshenv",
30385
+ ".zprofile",
30386
+ ".bashrc",
30387
+ ".bash_profile",
30388
+ ".bash_login",
30389
+ ".profile",
30390
+ ".gitconfig"
30391
+ ].map((f) => edit(at(home, f))),
30392
+ edit(`${gitHooks}/**`),
30393
+ edit(gitConfig),
30394
+ edit(`${at(runDir, ".claude")}/**`),
30395
+ ...walked.flatMap((e) => e.dir ? editTree(e.name) : [edit(e.name)]),
30396
+ ...toolDirs.filter((d) => d !== path2.parse(d).root).flatMap(editTree),
30397
+ ...BASH_DENY
30398
+ ];
30399
+ const allow = [
30400
+ "Bash",
30401
+ "mcp__notiamo__*",
30402
+ "mcp__notiamo-wp__*",
30403
+ `Read(${rulePath(runDir)}/**)`,
30404
+ "Glob",
30405
+ "Grep",
30406
+ `Edit(${rulePath(runDir)}/**)`,
30407
+ "WebFetch(domain:github.com)"
30408
+ ];
30409
+ const permissions = { blockReadsOutsideWorkingDirectories: true, blockWritesOutsideWorkingDirectories: true, allow, deny: [...new Set(deny)] };
30410
+ if (o.platform === "win32") return { permissions };
30411
+ const domains = [...DEFAULT_DOMAINS, o.notiamoHost, ...o.wordpressHost ? [o.wordpressHost] : [], ...o.extraDomains].map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
30412
+ return {
30413
+ permissions,
30414
+ sandbox: {
30415
+ enabled: true,
30416
+ failIfUnavailable: true,
30417
+ allowUnsandboxedCommands: false,
30418
+ filesystem: {
30419
+ denyRead: [...new Set(denyRead)],
30420
+ allowWrite: [runDir],
30421
+ denyWrite: [gitHooks, gitConfig]
30422
+ },
30423
+ network: {
30424
+ allowedDomains: [...new Set(domains)],
30425
+ // macOS: lets the engine's shell commands reach the run's local helper on 127.0.0.1 (no effect on Linux).
30426
+ ...o.wordpressHost ? { allowLocalBinding: true } : {}
30427
+ }
30428
+ }
30429
+ };
30430
+ }
30071
30431
 
30072
30432
  // src/service/darwin.ts
30073
30433
  var darwin_exports = {};
@@ -30079,8 +30439,8 @@ __export(darwin_exports, {
30079
30439
  uninstall: () => uninstall
30080
30440
  });
30081
30441
  import { setTimeout as delay } from "node:timers/promises";
30082
- import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2, existsSync } from "node:fs";
30083
- import path2 from "node:path";
30442
+ import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
30443
+ import path3 from "node:path";
30084
30444
 
30085
30445
  // src/service/common.ts
30086
30446
  var startArgs = (profile) => profile ? ["start", "--profile", profile] : ["start"];
@@ -30095,7 +30455,7 @@ function must(what, r) {
30095
30455
 
30096
30456
  // src/service/darwin.ts
30097
30457
  var label = (profile) => profile ? `com.notiamo.runner.${profile}` : "com.notiamo.runner";
30098
- var plistPath = (home, profile) => path2.join(home, "Library", "LaunchAgents", `${label(profile)}.plist`);
30458
+ var plistPath = (home, profile) => path3.join(home, "Library", "LaunchAgents", `${label(profile)}.plist`);
30099
30459
  var domain = (uid) => `gui/${uid}`;
30100
30460
  var BOOTSTRAP_TRIES = 5;
30101
30461
  var BOOTSTRAP_RETRY_MS = 500;
@@ -30135,7 +30495,7 @@ ${env}
30135
30495
  }
30136
30496
  async function install(o) {
30137
30497
  const file2 = plistPath(o.home, o.profile);
30138
- mkdirSync2(path2.dirname(file2), { recursive: true });
30498
+ mkdirSync2(path3.dirname(file2), { recursive: true });
30139
30499
  writeFileSync2(file2, renderPlist({ nodePath: o.nodePath, cliPath: o.cliPath, log: o.paths.log, env: o.env, profile: o.profile }));
30140
30500
  await o.exec("launchctl", ["bootout", domain(o.uid), file2]);
30141
30501
  const sleep = o.sleep ?? ((ms) => delay(ms));
@@ -30156,7 +30516,7 @@ async function uninstall(o) {
30156
30516
  rmSync(plistPath(o.home, o.profile), { force: true });
30157
30517
  }
30158
30518
  async function status(o) {
30159
- if (!existsSync(plistPath(o.home, o.profile))) return "not_installed";
30519
+ if (!existsSync2(plistPath(o.home, o.profile))) return "not_installed";
30160
30520
  const r = await o.exec("launchctl", ["print", `${domain(o.uid)}/${label(o.profile)}`]);
30161
30521
  return r.code === 0 ? "running" : "stopped";
30162
30522
  }
@@ -30170,10 +30530,10 @@ __export(linux_exports, {
30170
30530
  stop: () => stop2,
30171
30531
  uninstall: () => uninstall2
30172
30532
  });
30173
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
30174
- import path3 from "node:path";
30533
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
30534
+ import path4 from "node:path";
30175
30535
  var unit = (profile) => profile ? `notiamo-runner-${profile}` : "notiamo-runner";
30176
- var unitPath = (home, profile) => path3.join(home, ".config", "systemd", "user", `${unit(profile)}.service`);
30536
+ var unitPath = (home, profile) => path4.join(home, ".config", "systemd", "user", `${unit(profile)}.service`);
30177
30537
  var q = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
30178
30538
  function renderUnit(o) {
30179
30539
  const env = Object.entries(serviceEnv(o.env)).map(([k, v]) => `Environment=${q(`${k}=${v}`)}`).join("\n");
@@ -30195,7 +30555,7 @@ WantedBy=default.target
30195
30555
  }
30196
30556
  async function install2(o) {
30197
30557
  const file2 = unitPath(o.home, o.profile);
30198
- mkdirSync3(path3.dirname(file2), { recursive: true });
30558
+ mkdirSync3(path4.dirname(file2), { recursive: true });
30199
30559
  writeFileSync3(file2, renderUnit({ nodePath: o.nodePath, cliPath: o.cliPath, log: o.paths.log, env: o.env, profile: o.profile }));
30200
30560
  must("systemctl --user daemon-reload", await o.exec("systemctl", ["--user", "daemon-reload"]));
30201
30561
  must("systemctl --user enable", await o.exec("systemctl", ["--user", "enable", unit(o.profile)]));
@@ -30211,7 +30571,7 @@ async function uninstall2(o) {
30211
30571
  await o.exec("systemctl", ["--user", "daemon-reload"]);
30212
30572
  }
30213
30573
  async function status2(o) {
30214
- if (!existsSync2(unitPath(o.home, o.profile))) return "not_installed";
30574
+ if (!existsSync3(unitPath(o.home, o.profile))) return "not_installed";
30215
30575
  const r = await o.exec("systemctl", ["--user", "is-active", unit(o.profile)]);
30216
30576
  return r.stdout.trim() === "active" ? "running" : "stopped";
30217
30577
  }
@@ -30262,6 +30622,7 @@ var serviceStatus = async (o) => impl(o).status(withUid(o));
30262
30622
 
30263
30623
  // src/init.ts
30264
30624
  var DEFAULT_HOST = "https://my.notiamo.com";
30625
+ var KEYCHAIN_WARNING = "Warning: this user's keychain holds a GitHub login. A run on this machine could reach it. Remove it (Keychain Access, search github.com) or install the runner under a separate macOS user. The runner itself does not need it.";
30265
30626
  var runInteractive = (file2, args, env) => new Promise((resolve, reject) => {
30266
30627
  const child = spawn3(file2, args, { stdio: "inherit", env, shell: process.platform === "win32" });
30267
30628
  child.on("error", reject);
@@ -30281,11 +30642,9 @@ async function runInit(deps) {
30281
30642
  const token = (await deps.prompt("Runner token (made in Notiamo under Settings \u203A Agent runs \u203A Runners; nothing shows while you paste it): ", { hidden: true })).trim();
30282
30643
  if (!token.startsWith("ntr_")) throw new Error("That is not a runner token: it starts with ntr_. Make one in Notiamo: Settings \u203A Agent runs \u203A Runners.");
30283
30644
  const host = normalizeHost(await ask("Notiamo address: ", DEFAULT_HOST));
30284
- const hostname3 = deps.hostname ?? os2.hostname();
30645
+ const hostname3 = deps.hostname ?? os4.hostname();
30285
30646
  const name = await ask("Name of this runner: ", hostname3);
30286
- const version2 = await deps.exec("claude", ["--version"], { env });
30287
- if (version2.code !== 0) throw new Error("Claude Code is not installed. Install it first: https://claude.com/claude-code");
30288
- const engineVersion = version2.stdout.trim().slice(0, 100) || "unknown";
30647
+ const engineVersion = await checkMachine(deps, env, platform, print);
30289
30648
  try {
30290
30649
  await new NotiamoApi({ host, token }, deps.fetch).hello({ version: VERSION, os: platform, engine: "claude", engineVersion });
30291
30650
  } catch (e) {
@@ -30299,26 +30658,84 @@ async function runInit(deps) {
30299
30658
  }
30300
30659
  const check2 = await deps.exec("claude", ["-p", "say ok"], { env: claudeEnv });
30301
30660
  if (check2.code !== 0) throw new Error("Claude Code is not signed in for the runner.");
30661
+ await warnKeychain(deps, env, platform, print);
30302
30662
  saveConfig({ host, token, name, dataDir: dir, engine: "claude" }, dir);
30303
30663
  await installService({
30304
30664
  platform,
30305
30665
  profile: deps.profile,
30306
30666
  paths: p,
30307
30667
  exec: deps.exec,
30308
- home: deps.home ?? os2.homedir(),
30668
+ home: deps.home ?? os4.homedir(),
30309
30669
  env,
30310
30670
  nodePath: deps.nodePath ?? process.execPath,
30311
30671
  cliPath: deps.cliPath ?? process.argv[1]
30312
30672
  });
30313
30673
  print(`Runner ${name}${deps.profile ? ` (profile ${deps.profile})` : ""} is installed and running. Logs: ${p.log}`);
30314
30674
  }
30675
+ async function checkMachine(deps, env, platform, print) {
30676
+ const version2 = await deps.exec("claude", ["--version"], { env });
30677
+ if (version2.code !== 0) throw new Error("Claude Code is not installed. Install it first: https://claude.com/claude-code");
30678
+ const engineVersion = version2.stdout.trim().slice(0, 100) || "unknown";
30679
+ if (engineTooOld(engineVersion)) throw new Error(`Claude Code 2.1.314 or newer is needed (yours: ${engineVersion}). Update it: claude update`);
30680
+ const git = await deps.exec("git", ["--version"], { env });
30681
+ const gitVersion = git.code === 0 ? /git version (\d+)\.(\d+)/.exec(git.stdout) ?? null : null;
30682
+ if (!gitVersion || Number(gitVersion[1]) < 2 || Number(gitVersion[1]) === 2 && Number(gitVersion[2]) < 31) {
30683
+ const yours = git.code === 0 ? git.stdout.trim().replace(/^git version /, "").slice(0, 60) || "unknown" : "not installed";
30684
+ throw new Error(`git 2.31 or newer is needed (yours: ${yours}). Update git.`);
30685
+ }
30686
+ if (platform === "linux") {
30687
+ const bwrap = await deps.exec("which", ["bwrap"], { env });
30688
+ const socat = await deps.exec("which", ["socat"], { env });
30689
+ if (bwrap.code !== 0 || socat.code !== 0) {
30690
+ throw new Error("The sandbox needs bubblewrap and socat. Install them (Debian/Ubuntu: sudo apt install bubblewrap socat), then run init again.");
30691
+ }
30692
+ } else if (platform === "win32") {
30693
+ print("Note: on Windows without WSL2, shell commands of a run are not sandboxed; Claude's own file tools are still limited to the run folder.");
30694
+ }
30695
+ return engineVersion;
30696
+ }
30697
+ async function warnKeychain(deps, env, platform, print) {
30698
+ if (platform !== "darwin") return;
30699
+ const git = await deps.exec("security", ["find-internet-password", "-s", "github.com"], { env, timeout: 1e4 });
30700
+ const gh = await deps.exec("security", ["find-generic-password", "-s", "gh:github.com"], { env, timeout: 1e4 });
30701
+ if (git.code === 0 || gh.code === 0) print(KEYCHAIN_WARNING);
30702
+ }
30703
+ async function runUpdate(deps) {
30704
+ const dir = deps.dir ?? profileDir(deps.profile);
30705
+ const platform = deps.platform ?? process.platform;
30706
+ const p = paths(dir);
30707
+ const env = deps.env ?? process.env;
30708
+ const print = deps.print ?? ((s) => console.log(s));
30709
+ const cfg = loadConfig(dir);
30710
+ const engineVersion = await checkMachine(deps, env, platform, print);
30711
+ try {
30712
+ await new NotiamoApi({ host: cfg.host, token: cfg.token }, deps.fetch).hello({ version: VERSION, os: platform, engine: "claude", engineVersion });
30713
+ } catch (e) {
30714
+ if (e instanceof ApiError && e.status === 401) throw new Error("The saved runner token is not accepted any more (revoked or deleted). Make a new runner in Notiamo and run init.");
30715
+ throw new Error(`Notiamo at ${cfg.host} did not answer: ${e instanceof Error ? e.message : String(e)}`);
30716
+ }
30717
+ const check2 = await deps.exec("claude", ["-p", "say ok"], { env: { ...env, CLAUDE_CONFIG_DIR: p.claudeConfigDir } });
30718
+ if (check2.code !== 0) throw new Error("Claude Code is not signed in for the runner. Run: notiamo-runner init");
30719
+ await warnKeychain(deps, env, platform, print);
30720
+ await installService({
30721
+ platform,
30722
+ profile: deps.profile,
30723
+ paths: p,
30724
+ exec: deps.exec,
30725
+ home: deps.home ?? os4.homedir(),
30726
+ env,
30727
+ nodePath: deps.nodePath ?? process.execPath,
30728
+ cliPath: deps.cliPath ?? process.argv[1]
30729
+ });
30730
+ print(`Runner ${cfg.name}${deps.profile ? ` (profile ${deps.profile})` : ""} now runs version ${VERSION}. Logs: ${p.log}`);
30731
+ }
30315
30732
 
30316
30733
  // src/log.ts
30317
30734
  import { appendFileSync, mkdirSync as mkdirSync4, renameSync, statSync } from "node:fs";
30318
- import path4 from "node:path";
30735
+ import path5 from "node:path";
30319
30736
  var MAX_BYTES = 10 * 1024 * 1024;
30320
30737
  function createLogger(file2) {
30321
- mkdirSync4(path4.dirname(file2), { recursive: true, mode: 448 });
30738
+ mkdirSync4(path5.dirname(file2), { recursive: true, mode: 448 });
30322
30739
  const write = (level, msg) => {
30323
30740
  try {
30324
30741
  if (statSync(file2).size > MAX_BYTES) renameSync(file2, file2 + ".1");
@@ -30331,18 +30748,110 @@ function createLogger(file2) {
30331
30748
  }
30332
30749
 
30333
30750
  // src/loop.ts
30334
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
30335
- import path10 from "node:path";
30751
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
30752
+ import path12 from "node:path";
30336
30753
  import { setTimeout as delay2 } from "node:timers/promises";
30337
30754
 
30755
+ // src/clones.ts
30756
+ import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
30757
+ import path6 from "node:path";
30758
+ var lastRunPath = (gitDir, projectId) => path6.join(gitDir, `${projectId}.last-run`);
30759
+ function touchLastRun(gitDir, projectId, now) {
30760
+ mkdirSync5(gitDir, { recursive: true, mode: 448 });
30761
+ const file2 = lastRunPath(gitDir, projectId);
30762
+ rmSync3(file2, { force: true });
30763
+ writeFileSync4(file2, now.toISOString() + "\n", { flag: "wx", mode: 384 });
30764
+ }
30765
+ var DAY_MS = 864e5;
30766
+ var SLOT_RE = /^\d+$/;
30767
+ var ID_RE = /^[\w-]+$/;
30768
+ function removeStaleClones(projectsDir, gitDir, keepDays, busyProjectIds, now, log) {
30769
+ const removed = [];
30770
+ let names;
30771
+ try {
30772
+ names = readdirSync2(projectsDir);
30773
+ } catch (e) {
30774
+ if (e.code !== "ENOENT") log.warn(`project folders not listed: ${errorText(e)}`);
30775
+ return removed;
30776
+ }
30777
+ const cutoff = now.getTime() - keepDays * DAY_MS;
30778
+ const isDir = (p) => {
30779
+ try {
30780
+ return lstatSync(p).isDirectory();
30781
+ } catch {
30782
+ return false;
30783
+ }
30784
+ };
30785
+ const gone = /* @__PURE__ */ new Set();
30786
+ const remove = (folder, label2, id, extra, why) => {
30787
+ try {
30788
+ rmSync3(folder, { recursive: true, force: true });
30789
+ for (const f of extra) rmSync3(f, { force: true });
30790
+ removed.push(label2);
30791
+ gone.add(id);
30792
+ log.info(`removed project folder ${label2}: ${why}`);
30793
+ } catch (e) {
30794
+ log.warn(`project folder ${label2} not removed: ${errorText(e)}`);
30795
+ }
30796
+ };
30797
+ const slots = names.filter((n) => SLOT_RE.test(n) && isDir(path6.join(projectsDir, n)));
30798
+ for (const name of names) {
30799
+ if (SLOT_RE.test(name) || !ID_RE.test(name) || busyProjectIds.has(name)) continue;
30800
+ const folder = path6.join(projectsDir, name);
30801
+ if (!isDir(folder)) continue;
30802
+ remove(folder, name, name, [path6.join(gitDir, `${name}.worktree.json`)], "left by an older runner");
30803
+ }
30804
+ for (const slot of slots) {
30805
+ let ids;
30806
+ try {
30807
+ ids = readdirSync2(path6.join(projectsDir, slot));
30808
+ } catch (e) {
30809
+ log.warn(`slot ${slot} not listed: ${errorText(e)}`);
30810
+ continue;
30811
+ }
30812
+ for (const id of ids) {
30813
+ if (!ID_RE.test(id) || busyProjectIds.has(id)) continue;
30814
+ const folder = path6.join(projectsDir, slot, id);
30815
+ try {
30816
+ const st = lstatSync(folder);
30817
+ if (!st.isDirectory()) continue;
30818
+ let lastUse = st.mtimeMs;
30819
+ try {
30820
+ const marker = lstatSync(lastRunPath(gitDir, id));
30821
+ if (marker.isFile()) lastUse = marker.mtimeMs;
30822
+ } catch {
30823
+ }
30824
+ if (lastUse >= cutoff) continue;
30825
+ } catch (e) {
30826
+ log.warn(`project folder ${slot}/${id} not checked: ${errorText(e)}`);
30827
+ continue;
30828
+ }
30829
+ remove(folder, `${slot}/${id}`, id, [path6.join(gitDir, `${id}.${slot}.worktree.json`)], `unused for more than ${keepDays} days`);
30830
+ }
30831
+ }
30832
+ for (const id of gone) {
30833
+ if (slots.some((s) => isDir(path6.join(projectsDir, s, id)))) continue;
30834
+ try {
30835
+ rmSync3(path6.join(gitDir, `${id}.git`), { recursive: true, force: true });
30836
+ rmSync3(lastRunPath(gitDir, id), { force: true });
30837
+ } catch (e) {
30838
+ log.warn(`mirror of ${id} not removed: ${errorText(e)}`);
30839
+ }
30840
+ }
30841
+ return removed;
30842
+ }
30843
+ var errorText = (e) => e instanceof Error ? e.message : String(e);
30844
+
30338
30845
  // src/run.ts
30339
- import { mkdirSync as mkdirSync6, mkdtempSync, rmSync as rmSync3 } from "node:fs";
30340
- import path9 from "node:path";
30846
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
30847
+ import path11 from "node:path";
30341
30848
 
30342
30849
  // src/project-folder.ts
30343
30850
  import { execFile } from "node:child_process";
30344
- import { existsSync as existsSync3, mkdirSync as mkdirSync5 } from "node:fs";
30345
- import path5 from "node:path";
30851
+ import { createHash } from "node:crypto";
30852
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
30853
+ import os5 from "node:os";
30854
+ import path7 from "node:path";
30346
30855
  import { promisify } from "node:util";
30347
30856
  var execFileAsync = async (file2, args, opts) => {
30348
30857
  const { stdout, stderr } = await promisify(execFile)(file2, args, {
@@ -30354,25 +30863,182 @@ var execFileAsync = async (file2, args, opts) => {
30354
30863
  });
30355
30864
  return { stdout: String(stdout), stderr: String(stderr) };
30356
30865
  };
30866
+ var SAFE_GIT = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false"];
30867
+ function gitEnv() {
30868
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: os5.devNull };
30869
+ for (const k of Object.keys(env)) if (/^GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+|PARAMETERS)$/.test(k) || k === "GIT_ASKPASS") delete env[k];
30870
+ return env;
30871
+ }
30872
+ function githubAuthEnv(token) {
30873
+ return {
30874
+ ...gitEnv(),
30875
+ GIT_CONFIG_COUNT: "1",
30876
+ GIT_CONFIG_KEY_0: `http.${GITHUB_URL}.extraheader`,
30877
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
30878
+ };
30879
+ }
30357
30880
  var CLONE_TIMEOUT_MS = 10 * 6e4;
30358
30881
  var GIT_TIMEOUT_MS = 2 * 6e4;
30359
- async function prepareGithubFolder(projectsDir, projectId, g, exec = execFileAsync) {
30360
- const folder = path5.join(projectsDir, projectId);
30361
- const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
30362
- const git = (args, timeout = GIT_TIMEOUT_MS) => exec("git", ["-C", folder, ...args], { env, timeout });
30363
- if (existsSync3(path5.join(folder, ".git"))) {
30364
- await git(["fetch", "origin", "--prune"]);
30882
+ var barePath = (gitDir, projectId) => path7.join(gitDir, `${projectId}.git`);
30883
+ var projectFolder = (projectsDir, slot, projectId) => path7.join(projectsDir, String(slot), projectId);
30884
+ var GITHUB_URL = "https://github.com/";
30885
+ async function prepareGithubFolder(dirs, projectId, g, token, exec = execFileAsync) {
30886
+ if (!/^[\w-]+$/.test(projectId)) throw new Error("runner_error");
30887
+ if (typeof g.cloneUrl !== "string" || !g.cloneUrl.startsWith(GITHUB_URL) || /\s/.test(g.cloneUrl)) throw new Error("runner_error");
30888
+ const slot = dirs.slot ?? 0;
30889
+ const slotDir = path7.join(dirs.projectsDir, String(slot));
30890
+ const folder = projectFolder(dirs.projectsDir, slot, projectId);
30891
+ const bare = barePath(dirs.gitDir, projectId);
30892
+ const saved = savedSnapshotPath(dirs.gitDir, projectId, slot);
30893
+ const env = gitEnv();
30894
+ const gitIn = (dir, args, timeout = GIT_TIMEOUT_MS) => exec("git", [...SAFE_GIT, "-C", dir, ...args], { env, timeout });
30895
+ if (existsSync4(path7.join(bare, "HEAD"))) {
30896
+ await gitIn(bare, ["remote", "set-url", "--", "origin", g.cloneUrl]);
30897
+ await exec("git", [...SAFE_GIT, "-C", bare, "fetch", "origin", "--prune", "+refs/heads/*:refs/heads/*"], { env: githubAuthEnv(await token()), timeout: CLONE_TIMEOUT_MS });
30898
+ } else {
30899
+ mkdirSync6(dirs.gitDir, { recursive: true, mode: 448 });
30900
+ await exec("git", [...SAFE_GIT, "clone", "--bare", "--", g.cloneUrl, bare], { cwd: dirs.gitDir, env: githubAuthEnv(await token()), timeout: CLONE_TIMEOUT_MS });
30901
+ }
30902
+ if (exists(folder)) {
30903
+ const snap2 = readSavedSnapshot(saved);
30904
+ let ok = snap2 !== null;
30905
+ if (snap2) {
30906
+ try {
30907
+ verifyGit(folder, snap2);
30908
+ } catch {
30909
+ ok = false;
30910
+ }
30911
+ }
30912
+ if (!ok) rmSync4(folder, { recursive: true, force: true });
30913
+ }
30914
+ if (existsSync4(path7.join(folder, ".git"))) {
30915
+ await gitIn(folder, ["fetch", "origin", "--prune"]);
30365
30916
  } else {
30366
- mkdirSync5(projectsDir, { recursive: true, mode: 448 });
30367
- await exec("git", ["clone", g.cloneUrl, folder], { cwd: projectsDir, env, timeout: CLONE_TIMEOUT_MS });
30917
+ mkdirSync6(slotDir, { recursive: true, mode: 448 });
30918
+ await exec("git", [...SAFE_GIT, "clone", "--no-hardlinks", "--", bare, folder], { cwd: slotDir, env, timeout: CLONE_TIMEOUT_MS });
30368
30919
  }
30369
- const { stdout } = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
30920
+ const { stdout } = await gitIn(folder, ["rev-parse", "--abbrev-ref", "HEAD"]);
30370
30921
  if (stdout.trim() !== g.branch) {
30371
- await git(["reset", "--hard"]);
30372
- await git(["clean", "-fd"]);
30373
- await git(["checkout", "-B", g.branch, `origin/${g.fromBranch ?? g.defaultBranch}`]);
30922
+ await gitIn(folder, ["reset", "--hard"]);
30923
+ await gitIn(folder, ["clean", "-fd"]);
30924
+ await gitIn(folder, ["checkout", "-B", g.branch, `origin/${g.fromBranch ?? g.defaultBranch}`]);
30925
+ }
30926
+ const snap = snapshotGit(folder);
30927
+ saveSnapshot(dirs.gitDir, projectId, slot, snap);
30928
+ return { folder, snap };
30929
+ }
30930
+ var savedSnapshotPath = (gitDir, projectId, slot) => path7.join(gitDir, `${projectId}.${slot}.worktree.json`);
30931
+ function saveSnapshot(gitDir, projectId, slot, snap) {
30932
+ if (snap.missing) return;
30933
+ mkdirSync6(gitDir, { recursive: true, mode: 448 });
30934
+ const body = { gitRealPath: snap.real, dev: snap.dev, ino: snap.ino, configHash: snap.configHash };
30935
+ writeFileSync5(savedSnapshotPath(gitDir, projectId, slot), JSON.stringify(body) + "\n", { mode: 384 });
30936
+ }
30937
+ function forgetSnapshot(gitDir, projectId, slot) {
30938
+ rmSync4(savedSnapshotPath(gitDir, projectId, slot), { force: true });
30939
+ }
30940
+ function readSavedSnapshot(file2) {
30941
+ try {
30942
+ const j = JSON.parse(readFileSync2(file2, "utf8"));
30943
+ if (typeof j.gitRealPath !== "string" || typeof j.dev !== "number" || typeof j.ino !== "number" || typeof j.configHash !== "string") return null;
30944
+ return { missing: false, real: j.gitRealPath, dev: j.dev, ino: j.ino, configHash: j.configHash };
30945
+ } catch {
30946
+ return null;
30947
+ }
30948
+ }
30949
+ async function pushBranch(folder, bare, branch, token, exec = execFileAsync) {
30950
+ const env = gitEnv();
30951
+ await exec("git", [...SAFE_GIT, "-C", bare, "fetch", folder, `+refs/heads/${branch}:refs/heads/${branch}`], { env, timeout: GIT_TIMEOUT_MS }).catch(gitError);
30952
+ const auth = githubAuthEnv(await token());
30953
+ await exec("git", [...SAFE_GIT, "-C", bare, "push", "--no-verify", "origin", branch], { env: auth, timeout: GIT_TIMEOUT_MS }).catch(gitError);
30954
+ }
30955
+ function gitError(e) {
30956
+ const stderr = typeof e.stderr === "string" ? e.stderr : "";
30957
+ const line = stderr.split("\n").map((l) => l.trim()).find(Boolean);
30958
+ throw new Error(line ?? "git push failed");
30959
+ }
30960
+ var RepoTamperedError = class extends Error {
30961
+ constructor(detail) {
30962
+ super("repo_tampered");
30963
+ this.detail = detail;
30964
+ }
30965
+ detail;
30966
+ };
30967
+ function snapshotGit(folder) {
30968
+ const dir = path7.join(folder, ".git");
30969
+ let st;
30970
+ try {
30971
+ st = lstatSync2(dir);
30972
+ } catch {
30973
+ return { missing: true };
30974
+ }
30975
+ if (!st.isDirectory()) throw new RepoTamperedError(".git is not a folder");
30976
+ return { missing: false, ino: st.ino, dev: st.dev, real: realpathSync2(dir), configHash: configHash(dir) };
30977
+ }
30978
+ function verifyGit(folder, snap) {
30979
+ const dir = path7.join(folder, ".git");
30980
+ let st;
30981
+ try {
30982
+ st = lstatSync2(dir);
30983
+ } catch {
30984
+ if (snap.missing) return;
30985
+ throw new RepoTamperedError(".git is gone");
30986
+ }
30987
+ if (snap.missing) throw new RepoTamperedError(".git appeared");
30988
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new RepoTamperedError(".git is not a folder");
30989
+ if (st.ino !== snap.ino || st.dev !== snap.dev || realpathSync2(dir) !== snap.real) throw new RepoTamperedError(".git was replaced");
30990
+ if (existsSync4(path7.join(dir, "commondir"))) throw new RepoTamperedError(".git has a commondir file");
30991
+ for (const f of [path7.join("objects", "info", "alternates"), path7.join("objects", "info", "http-alternates"), "worktrees"]) {
30992
+ if (exists(path7.join(dir, f))) throw new RepoTamperedError(`.git/${f} exists`);
30993
+ }
30994
+ const link = findSymlink(dir);
30995
+ if (link) throw new RepoTamperedError(`.git/${link} is a symlink`);
30996
+ if (configHash(dir) !== snap.configHash) throw new RepoTamperedError(".git/config changed");
30997
+ }
30998
+ var WALK_MAX_DEPTH = 12;
30999
+ var WALK_MAX_ENTRIES = 2e5;
31000
+ function findSymlink(dir) {
31001
+ let count = 0;
31002
+ const walk = (rel, depth) => {
31003
+ if (depth > WALK_MAX_DEPTH) throw new RepoTamperedError(`.git is nested too deep: ${rel}`);
31004
+ let entries;
31005
+ try {
31006
+ entries = readdirSync3(path7.join(dir, rel), { withFileTypes: true });
31007
+ } catch {
31008
+ throw new RepoTamperedError(`unreadable entry in .git: ${rel || "."}`);
31009
+ }
31010
+ count += entries.length;
31011
+ if (count > WALK_MAX_ENTRIES) throw new RepoTamperedError(".git has too many entries");
31012
+ for (const e of entries) {
31013
+ const child = rel ? `${rel}/${e.name}` : e.name;
31014
+ if (e.isSymbolicLink()) return child;
31015
+ if (e.isDirectory()) {
31016
+ const found = walk(child, depth + 1);
31017
+ if (found) return found;
31018
+ }
31019
+ }
31020
+ return null;
31021
+ };
31022
+ return walk("", 0);
31023
+ }
31024
+ function configHash(gitDir) {
31025
+ const file2 = path7.join(gitDir, "config");
31026
+ let st;
31027
+ try {
31028
+ st = lstatSync2(file2);
31029
+ } catch {
31030
+ return "none";
31031
+ }
31032
+ if (!st.isFile()) throw new RepoTamperedError(".git/config is not a file");
31033
+ return createHash("sha256").update(readFileSync2(file2)).digest("hex");
31034
+ }
31035
+ function exists(p) {
31036
+ try {
31037
+ lstatSync2(p);
31038
+ return true;
31039
+ } catch {
31040
+ return false;
30374
31041
  }
30375
- return folder;
30376
31042
  }
30377
31043
 
30378
31044
  // src/scrub.ts
@@ -30444,12 +31110,12 @@ function resolveRoute(siteUrl, route) {
30444
31110
  return { error: "route is not a valid URL path" };
30445
31111
  }
30446
31112
  if (url2.origin !== site.origin || !url2.pathname.startsWith(prefix + "/wp/v2/")) return { error: "route must stay under /wp/v2/" };
30447
- const path11 = url2.pathname.slice(prefix.length);
30448
- return { path: path11, route: path11 + url2.search, query: url2.searchParams };
31113
+ const path13 = url2.pathname.slice(prefix.length);
31114
+ return { path: path13, route: path13 + url2.search, query: url2.searchParams };
30449
31115
  }
30450
31116
 
30451
31117
  // src/wp/backup.ts
30452
- import path6 from "node:path";
31118
+ import path8 from "node:path";
30453
31119
 
30454
31120
  // src/wp/transport.ts
30455
31121
  var shellQuote = (a) => /^[A-Za-z0-9_\-.,/:=@%+]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`;
@@ -30468,7 +31134,7 @@ async function backupBefore(t, root) {
30468
31134
  const wp = await dump(`{ wp db export - --path=${shellQuote(root)} || echo ${FAILED} >&2; } | gzip`);
30469
31135
  if (wp === "overflow") throw new Error("backup_failed: the database dump is over 200 MB");
30470
31136
  if (wp) return { dump: wp, how: "wp-cli" };
30471
- const config2 = await t.getFile(path6.posix.join(root, "wp-config.php")).catch(() => null);
31137
+ const config2 = await t.getFile(path8.posix.join(root, "wp-config.php")).catch(() => null);
30472
31138
  const db = config2 ? dbCredentials(config2) : null;
30473
31139
  if (!db) throw new Error("backup_failed: no wp-cli export and no database credentials in wp-config.php");
30474
31140
  const [host, port] = splitHost(db.host);
@@ -30600,7 +31266,7 @@ async function ftpTransport(conn) {
30600
31266
  // src/wp/helper.ts
30601
31267
  import { randomBytes, timingSafeEqual } from "node:crypto";
30602
31268
  import http from "node:http";
30603
- import path7 from "node:path";
31269
+ import path9 from "node:path";
30604
31270
 
30605
31271
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/util.js
30606
31272
  var util;
@@ -30976,8 +31642,8 @@ function getErrorMap() {
30976
31642
 
30977
31643
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/parseUtil.js
30978
31644
  var makeIssue = (params) => {
30979
- const { data, path: path11, errorMaps, issueData } = params;
30980
- const fullPath = [...path11, ...issueData.path || []];
31645
+ const { data, path: path13, errorMaps, issueData } = params;
31646
+ const fullPath = [...path13, ...issueData.path || []];
30981
31647
  const fullIssue = {
30982
31648
  ...issueData,
30983
31649
  path: fullPath
@@ -31092,11 +31758,11 @@ var errorUtil;
31092
31758
 
31093
31759
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/types.js
31094
31760
  var ParseInputLazyPath = class {
31095
- constructor(parent, value, path11, key) {
31761
+ constructor(parent, value, path13, key) {
31096
31762
  this._cachedPath = [];
31097
31763
  this.parent = parent;
31098
31764
  this.data = value;
31099
- this._path = path11;
31765
+ this._path = path13;
31100
31766
  this._key = key;
31101
31767
  }
31102
31768
  get path() {
@@ -35051,10 +35717,10 @@ function mergeDefs(...defs) {
35051
35717
  function cloneDef(schema) {
35052
35718
  return mergeDefs(schema._zod.def);
35053
35719
  }
35054
- function getElementAtPath(obj, path11) {
35055
- if (!path11)
35720
+ function getElementAtPath(obj, path13) {
35721
+ if (!path13)
35056
35722
  return obj;
35057
- return path11.reduce((acc, key) => acc?.[key], obj);
35723
+ return path13.reduce((acc, key) => acc?.[key], obj);
35058
35724
  }
35059
35725
  function promiseAllObject(promisesObj) {
35060
35726
  const keys = Object.keys(promisesObj);
@@ -35394,11 +36060,11 @@ function explicitlyAborted(x, startIndex = 0) {
35394
36060
  }
35395
36061
  return false;
35396
36062
  }
35397
- function prefixIssues(path11, issues) {
36063
+ function prefixIssues(path13, issues) {
35398
36064
  return issues.map((iss) => {
35399
36065
  var _a3;
35400
36066
  (_a3 = iss).path ?? (_a3.path = []);
35401
- iss.path.unshift(path11);
36067
+ iss.path.unshift(path13);
35402
36068
  return iss;
35403
36069
  });
35404
36070
  }
@@ -35860,16 +36526,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
35860
36526
  }
35861
36527
  function formatError(error62, mapper = (issue2) => issue2.message) {
35862
36528
  const fieldErrors = { _errors: [] };
35863
- const processError = (error63, path11 = []) => {
36529
+ const processError = (error63, path13 = []) => {
35864
36530
  for (const issue2 of error63.issues) {
35865
36531
  if (issue2.code === "invalid_union" && issue2.errors.length) {
35866
- issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
36532
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
35867
36533
  } else if (issue2.code === "invalid_key") {
35868
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36534
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35869
36535
  } else if (issue2.code === "invalid_element") {
35870
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36536
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35871
36537
  } else {
35872
- const fullpath = [...path11, ...issue2.path];
36538
+ const fullpath = [...path13, ...issue2.path];
35873
36539
  if (fullpath.length === 0) {
35874
36540
  fieldErrors._errors.push(mapper(issue2));
35875
36541
  } else {
@@ -35908,17 +36574,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
35908
36574
  }
35909
36575
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
35910
36576
  const result = { errors: [] };
35911
- const processError = (error63, path11 = []) => {
36577
+ const processError = (error63, path13 = []) => {
35912
36578
  var _a3;
35913
36579
  for (const issue2 of error63.issues) {
35914
36580
  if (issue2.code === "invalid_union" && issue2.errors.length) {
35915
- issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
36581
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
35916
36582
  } else if (issue2.code === "invalid_key") {
35917
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36583
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35918
36584
  } else if (issue2.code === "invalid_element") {
35919
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36585
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35920
36586
  } else {
35921
- const fullpath = [...path11, ...issue2.path];
36587
+ const fullpath = [...path13, ...issue2.path];
35922
36588
  if (fullpath.length === 0) {
35923
36589
  result.errors.push(mapper(issue2));
35924
36590
  continue;
@@ -35957,8 +36623,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
35957
36623
  }
35958
36624
  function toDotPath(_path) {
35959
36625
  const segs = [];
35960
- const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
35961
- for (const seg of path11) {
36626
+ const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36627
+ for (const seg of path13) {
35962
36628
  if (typeof seg === "number")
35963
36629
  segs.push(`[${seg}]`);
35964
36630
  else if (typeof seg === "symbol")
@@ -51509,11 +52175,11 @@ function normalizeObjectSchema(schema) {
51509
52175
  }
51510
52176
  return void 0;
51511
52177
  }
51512
- function getDotPath(path11) {
51513
- if (path11.length === 0) {
52178
+ function getDotPath(path13) {
52179
+ if (path13.length === 0) {
51514
52180
  return "object root";
51515
52181
  }
51516
- return path11.reduce((acc, seg, index) => {
52182
+ return path13.reduce((acc, seg, index) => {
51517
52183
  if (index === 0) {
51518
52184
  return String(seg);
51519
52185
  }
@@ -53740,13 +54406,13 @@ function resolveRef(ref, ctx) {
53740
54406
  if (!ref.startsWith("#")) {
53741
54407
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
53742
54408
  }
53743
- const path11 = ref.slice(1).split("/").filter(Boolean);
53744
- if (path11.length === 0) {
54409
+ const path13 = ref.slice(1).split("/").filter(Boolean);
54410
+ if (path13.length === 0) {
53745
54411
  return ctx.rootSchema;
53746
54412
  }
53747
54413
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
53748
- if (path11[0] === defsKey) {
53749
- const key = path11[1] === void 0 ? void 0 : decodeJSONPointerSegment(path11[1]);
54414
+ if (path13[0] === defsKey) {
54415
+ const key = path13[1] === void 0 ? void 0 : decodeJSONPointerSegment(path13[1]);
53750
54416
  if (!key || !ctx.defs[key]) {
53751
54417
  throw new Error(`Reference not found: ${ref}`);
53752
54418
  }
@@ -62194,7 +62860,7 @@ function buildServer(t) {
62194
62860
  if (!t.allow.files && !t.allow.all) return fail("the files group is off for this project");
62195
62861
  if (t.root === null) return fail("wordpress_root_not_found: the WordPress folder was not found on the server");
62196
62862
  const bad = pathRefusal(p, t.allow.all);
62197
- return bad ? fail(bad) : path7.posix.join(t.root, p);
62863
+ return bad ? fail(bad) : path9.posix.join(t.root, p);
62198
62864
  };
62199
62865
  server.registerTool("wp_get_file", {
62200
62866
  description: "Reads a file of the WordPress site, by its path relative to the WordPress folder (e.g. wp-content/themes/x/style.css).",
@@ -62266,9 +62932,9 @@ function apiRefusal(route, query, method, body, allow) {
62266
62932
  }
62267
62933
  function pathRefusal(p, all) {
62268
62934
  if (p.startsWith("/") || p.includes("\\") || p.includes("\0")) return "path must be relative to the WordPress folder";
62269
- const norm = path7.posix.normalize(p);
62935
+ const norm = path9.posix.normalize(p);
62270
62936
  if (norm === ".." || norm.startsWith("../") || p.split("/").includes("..")) return "path must stay inside the WordPress folder";
62271
- if (!all && PROTECTED_FILES.has(path7.posix.basename(norm))) return `refused: ${path7.posix.basename(norm)} is never changed unless "Allow everything" is on`;
62937
+ if (!all && PROTECTED_FILES.has(path9.posix.basename(norm))) return `refused: ${path9.posix.basename(norm)} is never changed unless "Allow everything" is on`;
62272
62938
  return null;
62273
62939
  }
62274
62940
  function publishedLink(json3) {
@@ -62280,7 +62946,7 @@ var cut2 = (s) => s.length > MAX_OUTPUT ? `${s.slice(0, MAX_OUTPUT)}
62280
62946
  [cut at ${MAX_OUTPUT} characters]` : s;
62281
62947
 
62282
62948
  // src/wp/root.ts
62283
- import path8 from "node:path";
62949
+ import path10 from "node:path";
62284
62950
  var FTP_CANDIDATES = [".", "public_html", "www", "htdocs", "httpdocs", "html", "web"];
62285
62951
  async function findRoot(t) {
62286
62952
  if (t.run) {
@@ -62288,10 +62954,10 @@ async function findRoot(t) {
62288
62954
  if (home?.code === 0) return ".";
62289
62955
  const found = await t.run(["find", ".", "-maxdepth", "3", "-name", "wp-config.php"]).catch(() => null);
62290
62956
  const first = found?.stdout.toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean).sort((a, b) => a.split("/").length - b.split("/").length)[0];
62291
- return first ? path8.posix.normalize(path8.posix.dirname(first)) : null;
62957
+ return first ? path10.posix.normalize(path10.posix.dirname(first)) : null;
62292
62958
  }
62293
62959
  for (const dir of FTP_CANDIDATES) {
62294
- if (await t.getFile(path8.posix.join(dir, "wp-config.php")).catch(() => null) !== null) return dir;
62960
+ if (await t.getFile(path10.posix.join(dir, "wp-config.php")).catch(() => null) !== null) return dir;
62295
62961
  }
62296
62962
  return null;
62297
62963
  }
@@ -62390,29 +63056,45 @@ async function runOne(spec, deps) {
62390
63056
  return ms <= 0 ? 0 : Math.max(1, Math.ceil(ms / 6e4));
62391
63057
  };
62392
63058
  const sendDone = (input2) => doneWithRetry(spec.runId, input2, api, log, sleep);
63059
+ if (engineTooOld(deps.sandbox.engineVersion)) {
63060
+ log.error(`run ${spec.runId}: Claude Code ${deps.sandbox.engineVersion ?? "(unknown version)"} is too old; update it (claude update)`);
63061
+ await sendDone({ outcome: "failed", error: "engine_too_old", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText2(e)}`));
63062
+ return;
63063
+ }
62393
63064
  const secrets = [
62394
63065
  spec.mcp.accessToken,
62395
63066
  spec.mcp.refreshToken,
62396
63067
  spec.wordpress?.apiPassword,
62397
63068
  spec.wordpress?.ssh?.privateKey,
62398
63069
  spec.wordpress?.ssh?.password,
62399
- spec.wordpress?.ftp?.password
63070
+ spec.wordpress?.ftp?.password,
63071
+ ...tokenForms(spec.github?.token)
62400
63072
  ].filter((s) => Boolean(s));
63073
+ const gitToken = spec.github ? githubToken(spec.runId, spec.github, api, secrets, now) : void 0;
62401
63074
  const totals = { costUsd: 0, inputTokens: 0, outputTokens: 0 };
62402
63075
  const events = transcript(spec.runId, api, log, secrets, now, totals);
62403
63076
  let helper;
62404
63077
  let tempDir;
63078
+ let settingsFile;
63079
+ let clone2;
62405
63080
  let pendingDone = true;
63081
+ const slot = deps.slot ?? 0;
63082
+ let started = false;
62406
63083
  try {
63084
+ if (!/^[\w-]+$/.test(spec.projectId)) throw new Error("runner_error");
62407
63085
  let cwd;
62408
63086
  if (spec.kind === "github" && spec.github) {
62409
- cwd = await prepareGithubFolder(deps.paths.projectsDir, spec.projectId, spec.github, deps.exec);
63087
+ started = true;
63088
+ clone2 = await prepareGithubFolder({ ...deps.paths, slot }, spec.projectId, spec.github, gitToken, deps.exec);
63089
+ cwd = clone2.folder;
62410
63090
  } else {
62411
- const parent = path9.join(deps.paths.projectsDir, spec.projectId);
62412
- mkdirSync6(parent, { recursive: true, mode: 448 });
62413
- cwd = tempDir = mkdtempSync(path9.join(parent, "run-"));
63091
+ started = true;
63092
+ const parent = projectFolder(deps.paths.projectsDir, slot, spec.projectId);
63093
+ mkdirSync7(parent, { recursive: true, mode: 448 });
63094
+ cwd = tempDir = mkdtempSync(path11.join(parent, "run-"));
62414
63095
  if (deps.helperFactory) helper = await deps.helperFactory(spec);
62415
63096
  }
63097
+ settingsFile = writeSettings(spec, deps, cwd);
62416
63098
  const base = {
62417
63099
  cwd,
62418
63100
  systemPrompt: spec.systemPrompt,
@@ -62420,7 +63102,8 @@ async function runOne(spec, deps) {
62420
63102
  helper: helper ? { url: helper.url, token: helper.token } : void 0,
62421
63103
  env: {},
62422
63104
  maxMinutes: spec.maxRunMinutes,
62423
- claudeConfigDir: deps.paths.claudeConfigDir
63105
+ claudeConfigDir: deps.paths.claudeConfigDir,
63106
+ settingsFile
62424
63107
  };
62425
63108
  log.info(`run ${spec.runId}: start`);
62426
63109
  const timedOut = (sessionId) => ({ outcome: "failed", error: "timeout", costUsd: 0, inputTokens: 0, outputTokens: 0, sessionId });
@@ -62433,10 +63116,26 @@ async function runOne(spec, deps) {
62433
63116
  if (result.sessionId) totals.sessionId = result.sessionId;
62434
63117
  const site = skipped ? void 0 : await helper?.checkSite?.();
62435
63118
  await events.flush(totals);
62436
- const input2 = site && site !== "ok" ? { outcome: "failed", error: site, ...totals } : doneInput(result, totals, secrets);
63119
+ let input2 = site && site !== "ok" ? { outcome: "failed", error: site, ...totals } : doneInput(result, totals, secrets);
63120
+ let pushFailed = false;
63121
+ if (input2.outcome === "reported" && spec.kind === "github" && spec.github) {
63122
+ try {
63123
+ if (clone2) verifyGit(clone2.folder, clone2.snap);
63124
+ await pushBranch(cwd, barePath(deps.paths.gitDir, spec.projectId), spec.github.branch, gitToken, deps.exec);
63125
+ } catch (e) {
63126
+ pushFailed = true;
63127
+ if (e instanceof RepoTamperedError) {
63128
+ log.error(`run ${spec.runId}: not pushed: the run changed the repository's own settings (${e.detail})`);
63129
+ input2 = { outcome: "failed", error: "repo_tampered", ...totals };
63130
+ } else {
63131
+ log.error(`run ${spec.runId}: push failed: ${scrub(errorText2(e), secrets).slice(0, 200)}`);
63132
+ input2 = { outcome: "failed", error: "push_failed", ...totals };
63133
+ }
63134
+ }
63135
+ }
62437
63136
  const answer = await sendDone(input2);
62438
63137
  log.info(`run ${spec.runId}: done ${input2.outcome}${input2.error ? ` ${input2.error}` : ""}`);
62439
- if (!answer.followUp || skipped) {
63138
+ if (!answer.followUp || skipped || pushFailed) {
62440
63139
  pendingDone = false;
62441
63140
  break;
62442
63141
  }
@@ -62456,7 +63155,7 @@ async function runOne(spec, deps) {
62456
63155
  log.error(`run ${spec.runId}: ${message}`);
62457
63156
  if (pendingDone && helper?.checkSite) {
62458
63157
  const site = await helper.checkSite().catch((e) => {
62459
- log.warn(`run ${spec.runId}: site check failed: ${errorText(e)}`);
63158
+ log.warn(`run ${spec.runId}: site check failed: ${errorText2(e)}`);
62460
63159
  return "ok";
62461
63160
  });
62462
63161
  if (site !== "ok") message = site;
@@ -62464,20 +63163,98 @@ async function runOne(spec, deps) {
62464
63163
  if (pendingDone) {
62465
63164
  const usage = events.usage();
62466
63165
  await events.flush();
62467
- await sendDone({ outcome: "failed", error: message, ...usage }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText(e)}`));
63166
+ await sendDone({ outcome: "failed", error: message, ...usage }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText2(e)}`));
62468
63167
  }
62469
63168
  } finally {
62470
63169
  events.stop();
62471
- if (helper) await helper.stop().catch((e) => log.warn(`run ${spec.runId}: helper stop failed: ${errorText(e)}`));
63170
+ if (helper) await helper.stop().catch((e) => log.warn(`run ${spec.runId}: helper stop failed: ${errorText2(e)}`));
63171
+ if (clone2) {
63172
+ try {
63173
+ verifyGit(clone2.folder, clone2.snap);
63174
+ saveSnapshot(deps.paths.gitDir, spec.projectId, slot, clone2.snap);
63175
+ } catch (e) {
63176
+ log.warn(`run ${spec.runId}: removing the project folder: ${e instanceof RepoTamperedError ? e.detail : errorText2(e)}`);
63177
+ try {
63178
+ forgetSnapshot(deps.paths.gitDir, spec.projectId, slot);
63179
+ } catch {
63180
+ }
63181
+ try {
63182
+ rmSync5(clone2.folder, { recursive: true, force: true });
63183
+ } catch (err) {
63184
+ log.warn(`run ${spec.runId}: could not remove ${clone2.folder}: ${errorText2(err)}`);
63185
+ }
63186
+ }
63187
+ }
63188
+ if (started) {
63189
+ try {
63190
+ touchLastRun(deps.paths.gitDir, spec.projectId, now());
63191
+ } catch (e) {
63192
+ log.warn(`run ${spec.runId}: could not write the last-run marker: ${errorText2(e)}`);
63193
+ }
63194
+ }
63195
+ if (settingsFile) {
63196
+ try {
63197
+ rmSync5(settingsFile, { force: true });
63198
+ } catch (e) {
63199
+ log.warn(`run ${spec.runId}: could not remove ${settingsFile}: ${errorText2(e)}`);
63200
+ }
63201
+ }
62472
63202
  if (tempDir) {
62473
63203
  try {
62474
- rmSync3(tempDir, { recursive: true, force: true });
63204
+ rmSync5(tempDir, { recursive: true, force: true });
62475
63205
  } catch (e) {
62476
- log.warn(`run ${spec.runId}: could not remove ${tempDir}: ${errorText(e)}`);
63206
+ log.warn(`run ${spec.runId}: could not remove ${tempDir}: ${errorText2(e)}`);
62477
63207
  }
62478
63208
  }
62479
63209
  }
62480
63210
  }
63211
+ var tokenForms = (token) => token ? [token, Buffer.from(`x-access-token:${token}`).toString("base64")] : [];
63212
+ var TOKEN_MARGIN_MS = 5 * 6e4;
63213
+ function githubToken(runId, g, api, secrets, now) {
63214
+ let token = g.token;
63215
+ let expiresAt = Date.parse(g.tokenExpiresAt);
63216
+ return async () => {
63217
+ if (!token || !Number.isFinite(expiresAt) || expiresAt - TOKEN_MARGIN_MS <= now().getTime()) {
63218
+ const fresh = await api.gitToken(runId);
63219
+ if (typeof fresh?.token !== "string" || !fresh.token) throw new Error("no GitHub token from Notiamo");
63220
+ token = fresh.token;
63221
+ expiresAt = Date.parse(fresh.expiresAt);
63222
+ secrets.push(...tokenForms(token));
63223
+ }
63224
+ return token;
63225
+ };
63226
+ }
63227
+ function writeSettings(spec, deps, cwd) {
63228
+ const { sandbox } = deps;
63229
+ let wordpressHost;
63230
+ if (spec.wordpress) {
63231
+ try {
63232
+ wordpressHost = new URL(spec.wordpress.siteUrl).hostname;
63233
+ } catch {
63234
+ }
63235
+ }
63236
+ if (!/^[\w-]+$/.test(spec.runId)) throw new Error("bad run id");
63237
+ const dir = path11.resolve(deps.paths.runsDir);
63238
+ mkdirSync7(dir, { recursive: true, mode: 448 });
63239
+ const settings = sandboxSettings({
63240
+ platform: sandbox.platform,
63241
+ runnerRoot: sandbox.runnerRoot,
63242
+ claudeConfigDir: deps.paths.claudeConfigDir,
63243
+ runDir: cwd,
63244
+ notiamoHost: sandbox.notiamoHost,
63245
+ extraDomains: sandbox.allowedDomains(),
63246
+ wordpressHost,
63247
+ gitExecPath: sandbox.gitExecPath,
63248
+ ghPath: sandbox.ghPath
63249
+ });
63250
+ const file2 = path11.join(dir, `${spec.runId}.json`);
63251
+ writeFileSync6(file2, JSON.stringify(settings, null, 2) + "\n", { mode: 384 });
63252
+ try {
63253
+ chmodSync2(file2, 384);
63254
+ } catch {
63255
+ }
63256
+ return file2;
63257
+ }
62481
63258
  function wordpressHelperFactory(deps) {
62482
63259
  const { log } = deps;
62483
63260
  return async (spec) => {
@@ -62490,7 +63267,7 @@ function wordpressHelperFactory(deps) {
62490
63267
  const backup = await backupBefore(transport, root);
62491
63268
  if (backup.dump) {
62492
63269
  log.info(`run ${spec.runId}: database backed up with ${backup.how} (${backup.dump.byteLength} bytes gzip)`);
62493
- await uploadDump(deps.api, spec.runId, backup.dump).catch((e) => log.warn(`run ${spec.runId}: backup upload failed: ${errorText(e)}`));
63270
+ await uploadDump(deps.api, spec.runId, backup.dump).catch((e) => log.warn(`run ${spec.runId}: backup upload failed: ${errorText2(e)}`));
62494
63271
  } else {
62495
63272
  log.warn(`run ${spec.runId}: no database backup (${transport ? "WordPress folder not found" : "no SSH"})`);
62496
63273
  }
@@ -62548,12 +63325,12 @@ async function doneWithRetry(runId, input2, api, log, sleep) {
62548
63325
  const retryable = !(e instanceof ApiError) || e.status >= 500;
62549
63326
  const wait = DONE_BACKOFF_MS[attempt];
62550
63327
  if (!retryable || wait === void 0) throw e;
62551
- log.warn(`run ${runId}: done failed (${errorText(e)}); retrying in ${wait / 1e3} s`);
63328
+ log.warn(`run ${runId}: done failed (${errorText2(e)}); retrying in ${wait / 1e3} s`);
62552
63329
  await sleep(wait);
62553
63330
  }
62554
63331
  }
62555
63332
  }
62556
- var errorText = (e) => e instanceof Error ? e.message : String(e);
63333
+ var errorText2 = (e) => e instanceof Error ? e.message : String(e);
62557
63334
  function doneInput(r, totals, secrets) {
62558
63335
  const usage = { ...totals };
62559
63336
  if (r.outcome === "ended") return { outcome: "failed", error: "no_report", ...usage };
@@ -62579,7 +63356,7 @@ function transcript(runId, api, log, secrets, now, totals) {
62579
63356
  try {
62580
63357
  await api.events(runId, body);
62581
63358
  } catch (err) {
62582
- log.warn(`run ${runId}: dropped ${body.lines.length} event lines: ${errorText(err)}`);
63359
+ log.warn(`run ${runId}: dropped ${body.lines.length} event lines: ${errorText2(err)}`);
62583
63360
  }
62584
63361
  }
62585
63362
  };
@@ -62612,72 +63389,144 @@ function transcript(runId, api, log, secrets, now, totals) {
62612
63389
  }
62613
63390
 
62614
63391
  // src/loop.ts
63392
+ var defaultSettings = (notiamoHost) => ({ runsAtOnce: 1, keepClonesDays: 7, allowedDomains: [], notiamoHost });
63393
+ function applyHello(settings, answer, defaultHost) {
63394
+ const d = defaultSettings(defaultHost);
63395
+ settings.runsAtOnce = Number.isInteger(answer.runsAtOnce) && answer.runsAtOnce >= 1 ? answer.runsAtOnce : d.runsAtOnce;
63396
+ settings.keepClonesDays = typeof answer.keepClonesDays === "number" && Number.isFinite(answer.keepClonesDays) && answer.keepClonesDays > 0 ? answer.keepClonesDays : d.keepClonesDays;
63397
+ settings.allowedDomains = Array.isArray(answer.allowedDomains) ? answer.allowedDomains.filter((x) => typeof x === "string" && x.length > 0) : d.allowedDomains;
63398
+ settings.notiamoHost = typeof answer.notiamoHost === "string" && answer.notiamoHost ? answer.notiamoHost : d.notiamoHost;
63399
+ }
62615
63400
  var HELLO_MS = 6e4;
63401
+ var SLOT_FOLDERS = 3;
62616
63402
  var NETWORK_BACKOFF_MS = 5e3;
62617
63403
  var NEXT_WAIT_SECONDS = 25;
62618
63404
  async function startLoop(deps) {
62619
63405
  const { api, log, signal } = deps;
62620
63406
  const sleep = deps.sleep ?? ((ms) => delay2(ms, void 0, { signal }).catch(() => {
62621
63407
  }));
63408
+ const defaultHost = deps.sandbox.notiamoHost;
63409
+ const settings = deps.settings ?? defaultSettings(defaultHost);
63410
+ const clones = deps.clones ?? removeStaleClones;
63411
+ const running = /* @__PURE__ */ new Map();
63412
+ for (let i = 0; i < SLOT_FOLDERS; i++) {
63413
+ try {
63414
+ mkdirSync8(path12.join(deps.paths.projectsDir, String(i)), { recursive: true, mode: 448 });
63415
+ } catch (e) {
63416
+ log.warn(`slot folder ${i} not made: ${errorText3(e)}`);
63417
+ }
63418
+ }
63419
+ const freeSlot = () => {
63420
+ const used = new Set([...running.values()].map((r) => r.slot));
63421
+ let i = 0;
63422
+ while (used.has(i)) i++;
63423
+ return i;
63424
+ };
63425
+ const saveState = () => {
63426
+ try {
63427
+ writeState(deps.paths.stateFile, [...running.keys()]);
63428
+ } catch (e) {
63429
+ log.warn(`state file not written: ${errorText3(e)}`);
63430
+ }
63431
+ };
62622
63432
  const leftover = readState(deps.paths.stateFile);
62623
- if (leftover) {
62624
- log.warn(`run ${leftover}: the runner restarted during it`);
62625
- await api.done(leftover, { outcome: "failed", error: "runner_restarted", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch(() => {
63433
+ for (const runId of leftover) {
63434
+ log.warn(`run ${runId}: the runner restarted during it`);
63435
+ await api.done(runId, { outcome: "failed", error: "runner_restarted", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch(() => {
62626
63436
  });
62627
- clearState(deps.paths.stateFile);
62628
63437
  }
62629
- const hello = () => api.hello({ version: deps.version, os: deps.os, engine: deps.engineName ?? "claude", engineVersion: deps.engineVersion }).then(() => {
62630
- }, (e) => log.warn(`hello failed: ${errorText2(e)}`));
63438
+ if (leftover.length) {
63439
+ try {
63440
+ writeState(deps.paths.stateFile, []);
63441
+ } catch (e) {
63442
+ log.warn(`state file not cleared: ${errorText3(e)}`);
63443
+ }
63444
+ }
63445
+ let helloInFlight = false;
63446
+ const hello = async () => {
63447
+ if (helloInFlight) return;
63448
+ helloInFlight = true;
63449
+ try {
63450
+ await helloOnce();
63451
+ } finally {
63452
+ helloInFlight = false;
63453
+ }
63454
+ };
63455
+ const helloOnce = async () => {
63456
+ try {
63457
+ applyHello(settings, await api.hello({ version: deps.version, os: deps.os, engine: deps.engineName ?? "claude", engineVersion: deps.engineVersion }), defaultHost);
63458
+ } catch (e) {
63459
+ log.warn(`hello failed: ${errorText3(e)}`);
63460
+ }
63461
+ try {
63462
+ clones(deps.paths.projectsDir, deps.paths.gitDir, settings.keepClonesDays, new Set([...running.values()].map((r) => r.projectId)), /* @__PURE__ */ new Date(), log);
63463
+ } catch (e) {
63464
+ log.warn(`project folder clean-up failed: ${errorText3(e)}`);
63465
+ }
63466
+ };
62631
63467
  await hello();
62632
- const timer = setInterval(() => void hello(), HELLO_MS);
63468
+ const timer = setInterval(() => void hello(), deps.helloEveryMs ?? HELLO_MS);
62633
63469
  timer.unref();
62634
63470
  try {
62635
63471
  while (!signal?.aborted) {
63472
+ if (running.size >= settings.runsAtOnce) {
63473
+ await Promise.race([...running.values()].map((r) => r.ended));
63474
+ continue;
63475
+ }
62636
63476
  let spec;
62637
63477
  try {
62638
63478
  spec = await api.next(NEXT_WAIT_SECONDS, signal);
62639
63479
  } catch (e) {
62640
63480
  if (signal?.aborted) break;
62641
- log.warn(`next failed: ${errorText2(e)}`);
63481
+ log.warn(`next failed: ${errorText3(e)}`);
62642
63482
  await sleep(NETWORK_BACKOFF_MS);
62643
63483
  continue;
62644
63484
  }
62645
63485
  if (!spec) continue;
62646
- try {
62647
- writeState(deps.paths.stateFile, spec.runId);
62648
- } catch (e) {
62649
- log.warn(`state file not written: ${errorText2(e)}`);
62650
- }
62651
- try {
62652
- await runOne(spec, { engine: deps.engine, api, paths: deps.paths, log, helperFactory: deps.helperFactory });
62653
- } catch (e) {
62654
- log.error(`run ${spec.runId}: ${errorText2(e)}`);
63486
+ const s = spec;
63487
+ if (running.has(s.runId)) {
63488
+ log.warn(`run ${s.runId}: handed out again while it runs here; ignored`);
63489
+ continue;
62655
63490
  }
62656
- try {
62657
- clearState(deps.paths.stateFile);
62658
- } catch (e) {
62659
- log.warn(`state file not cleared: ${errorText2(e)}`);
63491
+ if ([...running.values()].some((r) => r.projectId === s.projectId)) {
63492
+ log.warn(`run ${s.runId}: refused: its project is already running here`);
63493
+ await api.done(s.runId, { outcome: "failed", error: "runner_busy", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch((e) => log.warn(`run ${s.runId}: done failed: ${errorText3(e)}`));
63494
+ continue;
62660
63495
  }
63496
+ const slot = freeSlot();
63497
+ const entry = { projectId: s.projectId, slot, ended: Promise.resolve() };
63498
+ running.set(s.runId, entry);
63499
+ saveState();
63500
+ const sandbox = { ...deps.sandbox, notiamoHost: settings.notiamoHost, allowedDomains: () => settings.allowedDomains };
63501
+ entry.ended = runOne(s, { engine: deps.engine, api, paths: deps.paths, slot, sandbox, log, helperFactory: deps.helperFactory }).catch((e) => log.error(`run ${s.runId}: ${errorText3(e)}`)).finally(() => {
63502
+ running.delete(s.runId);
63503
+ saveState();
63504
+ });
62661
63505
  }
63506
+ await Promise.all([...running.values()].map((r) => r.ended));
62662
63507
  } finally {
62663
63508
  clearInterval(timer);
62664
63509
  }
62665
63510
  }
62666
- var errorText2 = (e) => e instanceof Error ? e.message : String(e);
63511
+ var errorText3 = (e) => e instanceof Error ? e.message : String(e);
62667
63512
  function readState(file2) {
62668
63513
  try {
62669
- const s = JSON.parse(readFileSync2(file2, "utf8"));
62670
- return typeof s.busyRunId === "string" ? s.busyRunId : null;
63514
+ const s = JSON.parse(readFileSync3(file2, "utf8"));
63515
+ if (Array.isArray(s.busyRunIds)) return s.busyRunIds.filter((x) => typeof x === "string");
63516
+ return typeof s.busyRunId === "string" ? [s.busyRunId] : [];
62671
63517
  } catch {
62672
- return null;
63518
+ return [];
62673
63519
  }
62674
63520
  }
62675
- function writeState(file2, busyRunId) {
62676
- mkdirSync7(path10.dirname(file2), { recursive: true, mode: 448 });
62677
- writeFileSync4(file2, JSON.stringify({ busyRunId }) + "\n", { mode: 384 });
62678
- }
62679
- function clearState(file2) {
62680
- rmSync4(file2, { force: true });
63521
+ function writeState(file2, busyRunIds) {
63522
+ if (!busyRunIds.length) {
63523
+ rmSync6(file2, { force: true });
63524
+ return;
63525
+ }
63526
+ mkdirSync8(path12.dirname(file2), { recursive: true, mode: 448 });
63527
+ const tmp = `${file2}.tmp`;
63528
+ writeFileSync7(tmp, JSON.stringify({ busyRunIds }) + "\n", { mode: 384 });
63529
+ renameSync2(tmp, file2);
62681
63530
  }
62682
63531
 
62683
63532
  // src/commands.ts
@@ -62687,7 +63536,7 @@ var serviceOptions = (profile) => ({
62687
63536
  profile,
62688
63537
  paths: paths(profileDir(profile)),
62689
63538
  exec: realExec,
62690
- home: os3.homedir(),
63539
+ home: os6.homedir(),
62691
63540
  env: process.env,
62692
63541
  nodePath: process.execPath,
62693
63542
  cliPath: process.argv[1]
@@ -62715,6 +63564,9 @@ async function prompt(q2, opts = {}) {
62715
63564
  async function init({ profile } = {}) {
62716
63565
  await runInit({ prompt, exec: realExec, fetch, profile });
62717
63566
  }
63567
+ async function update({ profile } = {}) {
63568
+ await runUpdate({ exec: realExec, fetch, profile });
63569
+ }
62718
63570
  async function start({ profile } = {}) {
62719
63571
  const dir = profileDir(profile);
62720
63572
  const cfg = loadConfig(dir);
@@ -62724,6 +63576,20 @@ async function start({ profile } = {}) {
62724
63576
  const engine = fake ? fakeEngine([{ kind: "message", text: "Fake engine run." }, { kind: "result", costUsd: 0, inputTokens: 0, outputTokens: 0, sessionId: "fake-session" }], { report: true }) : claudeEngine({});
62725
63577
  const v = fake ? null : await realExec("claude", ["--version"], { env: { ...process.env, CLAUDE_CONFIG_DIR: p.claudeConfigDir } });
62726
63578
  const engineVersion = fake ? "fake" : v?.code === 0 && v.stdout.trim().slice(0, 100) || "unknown";
63579
+ const settings = defaultSettings(hostOf(cfg.host));
63580
+ const tools = await credentialTools(realExec);
63581
+ const sandbox = {
63582
+ platform: process.platform,
63583
+ runnerRoot: defaultDir(),
63584
+ get notiamoHost() {
63585
+ return settings.notiamoHost;
63586
+ },
63587
+ allowedDomains: () => settings.allowedDomains,
63588
+ // The fake engine ignores the settings file: it passes the check.
63589
+ engineVersion: fake ? ENGINE_MIN_VERSION : v?.code === 0 ? engineVersion : null,
63590
+ gitExecPath: tools.gitExecPath,
63591
+ ghPath: tools.ghPath
63592
+ };
62727
63593
  const ctrl = new AbortController();
62728
63594
  const stop5 = (signal) => {
62729
63595
  if (ctrl.signal.aborted) return;
@@ -62734,12 +63600,15 @@ async function start({ profile } = {}) {
62734
63600
  process.on("SIGTERM", () => stop5("SIGTERM"));
62735
63601
  process.on("SIGINT", () => stop5("SIGINT"));
62736
63602
  log.info(`runner ${cfg.name} ${VERSION} started (${engineVersion}), host ${cfg.host}`);
63603
+ if (engineTooOld(sandbox.engineVersion)) log.warn(`Claude Code ${engineVersion} is older than ${ENGINE_MIN_VERSION}: runs fail until it is updated (claude update)`);
62737
63604
  if (process.stdout.isTTY) console.log(`Runner ${cfg.name} is waiting for runs from ${cfg.host}. Logs: ${p.log}`);
62738
63605
  const api = new NotiamoApi(cfg);
62739
63606
  await startLoop({
62740
63607
  api,
62741
63608
  engine,
62742
63609
  paths: p,
63610
+ sandbox,
63611
+ settings,
62743
63612
  log,
62744
63613
  version: VERSION,
62745
63614
  os: process.platform,
@@ -62780,10 +63649,17 @@ function logs({ profile } = {}) {
62780
63649
  const lines2 = tail(paths(profileDir(profile)).log, 50);
62781
63650
  console.log(lines2.length ? lines2.join("\n") : "No log yet.");
62782
63651
  }
63652
+ function hostOf(url2) {
63653
+ try {
63654
+ return new URL(url2).hostname;
63655
+ } catch {
63656
+ return url2;
63657
+ }
63658
+ }
62783
63659
  function tail(file2, n) {
62784
63660
  let text;
62785
63661
  try {
62786
- text = readFileSync3(file2, "utf8");
63662
+ text = readFileSync4(file2, "utf8");
62787
63663
  } catch {
62788
63664
  return [];
62789
63665
  }
@@ -62796,6 +63672,7 @@ var HELP = `notiamo-runner ${VERSION}: runs Notiamo agent runs on this machine w
62796
63672
  Usage: notiamo-runner <command> [--profile <name>]
62797
63673
 
62798
63674
  init Set the runner up: token, Claude Code sign-in, start at login or boot
63675
+ update Move a runner that is set up to this version: keeps its token and sign-in
62799
63676
  start Run in the foreground (what the service runs)
62800
63677
  stop Stop the service until the next login or boot
62801
63678
  status Whether the service is installed and running, and the last log line
@@ -62818,6 +63695,8 @@ ${HELP}`);
62818
63695
  switch (cmd) {
62819
63696
  case "init":
62820
63697
  return init(o);
63698
+ case "update":
63699
+ return update(o);
62821
63700
  case "start":
62822
63701
  return start(o);
62823
63702
  case "stop":