@factiii/runner 0.0.1 → 0.2.0

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/Dockerfile.claude +25 -3
  2. package/dist/cli.js +2633 -838
  3. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -30,9 +30,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  mod
31
31
  ));
32
32
 
33
- // node_modules/commander/lib/error.js
33
+ // ../../node_modules/commander/lib/error.js
34
34
  var require_error = __commonJS({
35
- "node_modules/commander/lib/error.js"(exports2) {
35
+ "../../node_modules/commander/lib/error.js"(exports2) {
36
36
  var CommanderError2 = class extends Error {
37
37
  /**
38
38
  * Constructs the CommanderError class
@@ -65,9 +65,9 @@ var require_error = __commonJS({
65
65
  }
66
66
  });
67
67
 
68
- // node_modules/commander/lib/argument.js
68
+ // ../../node_modules/commander/lib/argument.js
69
69
  var require_argument = __commonJS({
70
- "node_modules/commander/lib/argument.js"(exports2) {
70
+ "../../node_modules/commander/lib/argument.js"(exports2) {
71
71
  var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
72
72
  var Argument2 = class {
73
73
  /**
@@ -192,9 +192,9 @@ var require_argument = __commonJS({
192
192
  }
193
193
  });
194
194
 
195
- // node_modules/commander/lib/help.js
195
+ // ../../node_modules/commander/lib/help.js
196
196
  var require_help = __commonJS({
197
- "node_modules/commander/lib/help.js"(exports2) {
197
+ "../../node_modules/commander/lib/help.js"(exports2) {
198
198
  var { humanReadableArgName } = require_argument();
199
199
  var Help2 = class {
200
200
  constructor() {
@@ -606,9 +606,9 @@ var require_help = __commonJS({
606
606
  }
607
607
  });
608
608
 
609
- // node_modules/commander/lib/option.js
609
+ // ../../node_modules/commander/lib/option.js
610
610
  var require_option = __commonJS({
611
- "node_modules/commander/lib/option.js"(exports2) {
611
+ "../../node_modules/commander/lib/option.js"(exports2) {
612
612
  var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
613
613
  var Option2 = class {
614
614
  /**
@@ -878,9 +878,9 @@ var require_option = __commonJS({
878
878
  }
879
879
  });
880
880
 
881
- // node_modules/commander/lib/suggestSimilar.js
881
+ // ../../node_modules/commander/lib/suggestSimilar.js
882
882
  var require_suggestSimilar = __commonJS({
883
- "node_modules/commander/lib/suggestSimilar.js"(exports2) {
883
+ "../../node_modules/commander/lib/suggestSimilar.js"(exports2) {
884
884
  var maxDistance = 3;
885
885
  function editDistance(a, b) {
886
886
  if (Math.abs(a.length - b.length) > maxDistance)
@@ -958,13 +958,13 @@ var require_suggestSimilar = __commonJS({
958
958
  }
959
959
  });
960
960
 
961
- // node_modules/commander/lib/command.js
961
+ // ../../node_modules/commander/lib/command.js
962
962
  var require_command = __commonJS({
963
- "node_modules/commander/lib/command.js"(exports2) {
963
+ "../../node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path6 = require("node:path");
967
- var fs4 = require("node:fs");
966
+ var path7 = require("node:path");
967
+ var fs6 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
970
970
  var { CommanderError: CommanderError2 } = require_error();
@@ -1896,11 +1896,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path6.resolve(baseDir, baseName);
1900
- if (fs4.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path6.extname(baseName))) return void 0;
1899
+ const localBin = path7.resolve(baseDir, baseName);
1900
+ if (fs6.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path7.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs4.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs6.existsSync(`${localBin}${ext}`)
1904
1904
  );
1905
1905
  if (foundExt) return `${localBin}${foundExt}`;
1906
1906
  return void 0;
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1912
1912
  if (this._scriptPath) {
1913
1913
  let resolvedScriptPath;
1914
1914
  try {
1915
- resolvedScriptPath = fs4.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs6.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path6.resolve(
1920
- path6.dirname(resolvedScriptPath),
1919
+ executableDir = path7.resolve(
1920
+ path7.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path6.basename(
1927
+ const legacyName = path7.basename(
1928
1928
  this._scriptPath,
1929
- path6.extname(this._scriptPath)
1929
+ path7.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path6.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path7.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process2.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path6.basename(filename, path6.extname(filename));
2780
+ this._name = path7.basename(filename, path7.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path7) {
2795
- if (path7 === void 0) return this._executableDir;
2796
- this._executableDir = path7;
2794
+ executableDir(path8) {
2795
+ if (path8 === void 0) return this._executableDir;
2796
+ this._executableDir = path8;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -3001,9 +3001,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3001
3001
  }
3002
3002
  });
3003
3003
 
3004
- // node_modules/commander/index.js
3004
+ // ../../node_modules/commander/index.js
3005
3005
  var require_commander = __commonJS({
3006
- "node_modules/commander/index.js"(exports2) {
3006
+ "../../node_modules/commander/index.js"(exports2) {
3007
3007
  var { Argument: Argument2 } = require_argument();
3008
3008
  var { Command: Command2 } = require_command();
3009
3009
  var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error();
@@ -3026,9 +3026,9 @@ var require_commander = __commonJS({
3026
3026
  // ../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3027
3027
  var require_XMLHttpRequest = __commonJS({
3028
3028
  "../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports2, module2) {
3029
- var fs4 = require("fs");
3029
+ var fs6 = require("fs");
3030
3030
  var Url = require("url");
3031
- var spawn2 = require("child_process").spawn;
3031
+ var spawn3 = require("child_process").spawn;
3032
3032
  module2.exports = XMLHttpRequest3;
3033
3033
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
3034
3034
  function XMLHttpRequest3(opts) {
@@ -3184,7 +3184,7 @@ var require_XMLHttpRequest = __commonJS({
3184
3184
  throw new Error("XMLHttpRequest: Only GET method is supported");
3185
3185
  }
3186
3186
  if (settings.async) {
3187
- fs4.readFile(unescape(url2.pathname), function(error, data2) {
3187
+ fs6.readFile(unescape(url2.pathname), function(error, data2) {
3188
3188
  if (error) {
3189
3189
  self.handleError(error, error.errno || -1);
3190
3190
  } else {
@@ -3196,7 +3196,7 @@ var require_XMLHttpRequest = __commonJS({
3196
3196
  });
3197
3197
  } else {
3198
3198
  try {
3199
- this.response = fs4.readFileSync(unescape(url2.pathname));
3199
+ this.response = fs6.readFileSync(unescape(url2.pathname));
3200
3200
  this.responseText = this.response.toString("utf8");
3201
3201
  this.status = 200;
3202
3202
  setState(self.DONE);
@@ -3322,15 +3322,15 @@ var require_XMLHttpRequest = __commonJS({
3322
3322
  } else {
3323
3323
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
3324
3324
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
3325
- fs4.writeFileSync(syncFile, "", "utf8");
3325
+ fs6.writeFileSync(syncFile, "", "utf8");
3326
3326
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
3327
- var syncProc = spawn2(process.argv[0], ["-e", execString]);
3327
+ var syncProc = spawn3(process.argv[0], ["-e", execString]);
3328
3328
  var statusText;
3329
- while (fs4.existsSync(syncFile)) {
3329
+ while (fs6.existsSync(syncFile)) {
3330
3330
  }
3331
- self.responseText = fs4.readFileSync(contentFile, "utf8");
3331
+ self.responseText = fs6.readFileSync(contentFile, "utf8");
3332
3332
  syncProc.stdin.end();
3333
- fs4.unlinkSync(contentFile);
3333
+ fs6.unlinkSync(contentFile);
3334
3334
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
3335
3335
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
3336
3336
  self.handleError(errorObj, 503);
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
3984
3984
  var require_supports_color = __commonJS({
3985
3985
  "../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
3986
3986
  "use strict";
3987
- var os3 = require("os");
3987
+ var os4 = require("os");
3988
3988
  var hasFlag = require_has_flag();
3989
3989
  var env = process.env;
3990
3990
  var forceColor;
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
4022
4022
  }
4023
4023
  const min = forceColor ? 1 : 0;
4024
4024
  if (process.platform === "win32") {
4025
- const osRelease = os3.release().split(".");
4025
+ const osRelease = os4.release().split(".");
4026
4026
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
4027
4027
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
4028
4028
  }
@@ -4269,6 +4269,7 @@ var require_constants = __commonJS({
4269
4269
  if (hasBlob) BINARY_TYPES.push("blob");
4270
4270
  module2.exports = {
4271
4271
  BINARY_TYPES,
4272
+ CLOSE_TIMEOUT: 3e4,
4272
4273
  EMPTY_BUFFER: Buffer.alloc(0),
4273
4274
  GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
4274
4275
  hasBlob,
@@ -4423,7 +4424,7 @@ var require_permessage_deflate = __commonJS({
4423
4424
  var kBuffers = Symbol("buffers");
4424
4425
  var kError = Symbol("error");
4425
4426
  var zlibLimiter;
4426
- var PerMessageDeflate = class {
4427
+ var PerMessageDeflate2 = class {
4427
4428
  /**
4428
4429
  * Creates a PerMessageDeflate instance.
4429
4430
  *
@@ -4434,6 +4435,9 @@ var require_permessage_deflate = __commonJS({
4434
4435
  * acknowledge disabling of client context takeover
4435
4436
  * @param {Number} [options.concurrencyLimit=10] The number of concurrent
4436
4437
  * calls to zlib
4438
+ * @param {Boolean} [options.isServer=false] Create the instance in either
4439
+ * server or client mode
4440
+ * @param {Number} [options.maxPayload=0] The maximum allowed message length
4437
4441
  * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
4438
4442
  * use of a custom server window size
4439
4443
  * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
@@ -4444,15 +4448,12 @@ var require_permessage_deflate = __commonJS({
4444
4448
  * deflate
4445
4449
  * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
4446
4450
  * inflate
4447
- * @param {Boolean} [isServer=false] Create the instance in either server or
4448
- * client mode
4449
- * @param {Number} [maxPayload=0] The maximum allowed message length
4450
4451
  */
4451
- constructor(options, isServer, maxPayload) {
4452
- this._maxPayload = maxPayload | 0;
4452
+ constructor(options) {
4453
4453
  this._options = options || {};
4454
4454
  this._threshold = this._options.threshold !== void 0 ? this._options.threshold : 1024;
4455
- this._isServer = !!isServer;
4455
+ this._maxPayload = this._options.maxPayload | 0;
4456
+ this._isServer = !!this._options.isServer;
4456
4457
  this._deflate = null;
4457
4458
  this._inflate = null;
4458
4459
  this.params = null;
@@ -4761,7 +4762,7 @@ var require_permessage_deflate = __commonJS({
4761
4762
  });
4762
4763
  }
4763
4764
  };
4764
- module2.exports = PerMessageDeflate;
4765
+ module2.exports = PerMessageDeflate2;
4765
4766
  function deflateOnData(chunk) {
4766
4767
  this[kBuffers].push(chunk);
4767
4768
  this[kTotalLength] += chunk.length;
@@ -4996,7 +4997,7 @@ var require_receiver = __commonJS({
4996
4997
  "../../node_modules/engine.io-client/node_modules/ws/lib/receiver.js"(exports2, module2) {
4997
4998
  "use strict";
4998
4999
  var { Writable } = require("stream");
4999
- var PerMessageDeflate = require_permessage_deflate();
5000
+ var PerMessageDeflate2 = require_permessage_deflate();
5000
5001
  var {
5001
5002
  BINARY_TYPES,
5002
5003
  EMPTY_BUFFER,
@@ -5163,7 +5164,7 @@ var require_receiver = __commonJS({
5163
5164
  return;
5164
5165
  }
5165
5166
  const compressed = (buf[0] & 64) === 64;
5166
- if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
5167
+ if (compressed && !this._extensions[PerMessageDeflate2.extensionName]) {
5167
5168
  const error = this.createError(
5168
5169
  RangeError,
5169
5170
  "RSV1 must be clear",
@@ -5407,7 +5408,7 @@ var require_receiver = __commonJS({
5407
5408
  * @private
5408
5409
  */
5409
5410
  decompress(data, cb) {
5410
- const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
5411
+ const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
5411
5412
  perMessageDeflate.decompress(data, this._fin, (err, buf) => {
5412
5413
  if (err) return cb(err);
5413
5414
  if (buf.length) {
@@ -5589,7 +5590,10 @@ var require_sender = __commonJS({
5589
5590
  "use strict";
5590
5591
  var { Duplex } = require("stream");
5591
5592
  var { randomFillSync } = require("crypto");
5592
- var PerMessageDeflate = require_permessage_deflate();
5593
+ var {
5594
+ types: { isUint8Array }
5595
+ } = require("util");
5596
+ var PerMessageDeflate2 = require_permessage_deflate();
5593
5597
  var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
5594
5598
  var { isBlob, isValidStatusCode } = require_validation();
5595
5599
  var { mask: applyMask, toBuffer: toBuffer2 } = require_buffer_util();
@@ -5742,8 +5746,10 @@ var require_sender = __commonJS({
5742
5746
  buf.writeUInt16BE(code, 0);
5743
5747
  if (typeof data === "string") {
5744
5748
  buf.write(data, 2);
5745
- } else {
5749
+ } else if (isUint8Array(data)) {
5746
5750
  buf.set(data, 2);
5751
+ } else {
5752
+ throw new TypeError("Second argument must be a string or a Uint8Array");
5747
5753
  }
5748
5754
  }
5749
5755
  const options = {
@@ -5873,7 +5879,7 @@ var require_sender = __commonJS({
5873
5879
  * @public
5874
5880
  */
5875
5881
  send(data, options, cb) {
5876
- const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
5882
+ const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
5877
5883
  let opcode = options.binary ? 2 : 1;
5878
5884
  let rsv1 = options.compress;
5879
5885
  let byteLength2;
@@ -5997,7 +6003,7 @@ var require_sender = __commonJS({
5997
6003
  this.sendFrame(_Sender.frame(data, options), cb);
5998
6004
  return;
5999
6005
  }
6000
- const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
6006
+ const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
6001
6007
  this._bufferedBytes += options[kByteLength];
6002
6008
  this._state = DEFLATING;
6003
6009
  perMessageDeflate.compress(data, options.fin, (_, buf) => {
@@ -6435,11 +6441,11 @@ var require_extension = __commonJS({
6435
6441
  return offers;
6436
6442
  }
6437
6443
  function format(extensions) {
6438
- return Object.keys(extensions).map((extension) => {
6439
- let configurations = extensions[extension];
6444
+ return Object.keys(extensions).map((extension2) => {
6445
+ let configurations = extensions[extension2];
6440
6446
  if (!Array.isArray(configurations)) configurations = [configurations];
6441
6447
  return configurations.map((params) => {
6442
- return [extension].concat(
6448
+ return [extension2].concat(
6443
6449
  Object.keys(params).map((k) => {
6444
6450
  let values = params[k];
6445
6451
  if (!Array.isArray(values)) values = [values];
@@ -6465,12 +6471,13 @@ var require_websocket = __commonJS({
6465
6471
  var { randomBytes, createHash } = require("crypto");
6466
6472
  var { Duplex, Readable } = require("stream");
6467
6473
  var { URL: URL2 } = require("url");
6468
- var PerMessageDeflate = require_permessage_deflate();
6474
+ var PerMessageDeflate2 = require_permessage_deflate();
6469
6475
  var Receiver2 = require_receiver();
6470
6476
  var Sender2 = require_sender();
6471
6477
  var { isBlob } = require_validation();
6472
6478
  var {
6473
6479
  BINARY_TYPES,
6480
+ CLOSE_TIMEOUT,
6474
6481
  EMPTY_BUFFER,
6475
6482
  GUID,
6476
6483
  kForOnEventAttribute,
@@ -6484,7 +6491,6 @@ var require_websocket = __commonJS({
6484
6491
  } = require_event_target();
6485
6492
  var { format, parse: parse3 } = require_extension();
6486
6493
  var { toBuffer: toBuffer2 } = require_buffer_util();
6487
- var closeTimeout = 30 * 1e3;
6488
6494
  var kAborted = Symbol("kAborted");
6489
6495
  var protocolVersions = [8, 13];
6490
6496
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
@@ -6530,6 +6536,7 @@ var require_websocket = __commonJS({
6530
6536
  initAsClient(this, address, protocols, options);
6531
6537
  } else {
6532
6538
  this._autoPong = options.autoPong;
6539
+ this._closeTimeout = options.closeTimeout;
6533
6540
  this._isServer = true;
6534
6541
  }
6535
6542
  }
@@ -6672,8 +6679,8 @@ var require_websocket = __commonJS({
6672
6679
  this.emit("close", this._closeCode, this._closeMessage);
6673
6680
  return;
6674
6681
  }
6675
- if (this._extensions[PerMessageDeflate.extensionName]) {
6676
- this._extensions[PerMessageDeflate.extensionName].cleanup();
6682
+ if (this._extensions[PerMessageDeflate2.extensionName]) {
6683
+ this._extensions[PerMessageDeflate2.extensionName].cleanup();
6677
6684
  }
6678
6685
  this._receiver.removeAllListeners();
6679
6686
  this._readyState = _WebSocket.CLOSED;
@@ -6835,7 +6842,7 @@ var require_websocket = __commonJS({
6835
6842
  fin: true,
6836
6843
  ...options
6837
6844
  };
6838
- if (!this._extensions[PerMessageDeflate.extensionName]) {
6845
+ if (!this._extensions[PerMessageDeflate2.extensionName]) {
6839
6846
  opts.compress = false;
6840
6847
  }
6841
6848
  this._sender.send(data || EMPTY_BUFFER, opts, cb);
@@ -6931,6 +6938,7 @@ var require_websocket = __commonJS({
6931
6938
  const opts = {
6932
6939
  allowSynchronousEvents: true,
6933
6940
  autoPong: true,
6941
+ closeTimeout: CLOSE_TIMEOUT,
6934
6942
  protocolVersion: protocolVersions[1],
6935
6943
  maxPayload: 100 * 1024 * 1024,
6936
6944
  skipUTF8Validation: false,
@@ -6948,6 +6956,7 @@ var require_websocket = __commonJS({
6948
6956
  port: void 0
6949
6957
  };
6950
6958
  websocket._autoPong = opts.autoPong;
6959
+ websocket._closeTimeout = opts.closeTimeout;
6951
6960
  if (!protocolVersions.includes(opts.protocolVersion)) {
6952
6961
  throw new RangeError(
6953
6962
  `Unsupported protocol version: ${opts.protocolVersion} (supported versions: ${protocolVersions.join(", ")})`
@@ -6959,7 +6968,7 @@ var require_websocket = __commonJS({
6959
6968
  } else {
6960
6969
  try {
6961
6970
  parsedUrl = new URL2(address);
6962
- } catch (e) {
6971
+ } catch {
6963
6972
  throw new SyntaxError(`Invalid URL: ${address}`);
6964
6973
  }
6965
6974
  }
@@ -7007,13 +7016,13 @@ var require_websocket = __commonJS({
7007
7016
  opts.path = parsedUrl.pathname + parsedUrl.search;
7008
7017
  opts.timeout = opts.handshakeTimeout;
7009
7018
  if (opts.perMessageDeflate) {
7010
- perMessageDeflate = new PerMessageDeflate(
7011
- opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
7012
- false,
7013
- opts.maxPayload
7014
- );
7019
+ perMessageDeflate = new PerMessageDeflate2({
7020
+ ...opts.perMessageDeflate,
7021
+ isServer: false,
7022
+ maxPayload: opts.maxPayload
7023
+ });
7015
7024
  opts.headers["Sec-WebSocket-Extensions"] = format({
7016
- [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
7025
+ [PerMessageDeflate2.extensionName]: perMessageDeflate.offer()
7017
7026
  });
7018
7027
  }
7019
7028
  if (protocols.length) {
@@ -7156,19 +7165,19 @@ var require_websocket = __commonJS({
7156
7165
  return;
7157
7166
  }
7158
7167
  const extensionNames = Object.keys(extensions);
7159
- if (extensionNames.length !== 1 || extensionNames[0] !== PerMessageDeflate.extensionName) {
7168
+ if (extensionNames.length !== 1 || extensionNames[0] !== PerMessageDeflate2.extensionName) {
7160
7169
  const message = "Server indicated an extension that was not requested";
7161
7170
  abortHandshake(websocket, socket, message);
7162
7171
  return;
7163
7172
  }
7164
7173
  try {
7165
- perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
7174
+ perMessageDeflate.accept(extensions[PerMessageDeflate2.extensionName]);
7166
7175
  } catch (err) {
7167
7176
  const message = "Invalid Sec-WebSocket-Extensions header";
7168
7177
  abortHandshake(websocket, socket, message);
7169
7178
  return;
7170
7179
  }
7171
- websocket._extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
7180
+ websocket._extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
7172
7181
  }
7173
7182
  websocket.setSocket(socket, head, {
7174
7183
  allowSynchronousEvents: opts.allowSynchronousEvents,
@@ -7290,7 +7299,7 @@ var require_websocket = __commonJS({
7290
7299
  function setCloseTimer(websocket) {
7291
7300
  websocket._closeTimer = setTimeout(
7292
7301
  websocket._socket.destroy.bind(websocket._socket),
7293
- closeTimeout
7302
+ websocket._closeTimeout
7294
7303
  );
7295
7304
  }
7296
7305
  function socketOnClose() {
@@ -7299,8 +7308,8 @@ var require_websocket = __commonJS({
7299
7308
  this.removeListener("data", socketOnData);
7300
7309
  this.removeListener("end", socketOnEnd);
7301
7310
  websocket._readyState = WebSocket2.CLOSING;
7302
- let chunk;
7303
- if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && (chunk = websocket._socket.read()) !== null) {
7311
+ if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
7312
+ const chunk = this.read(this._readableState.length);
7304
7313
  websocket._receiver.write(chunk);
7305
7314
  }
7306
7315
  websocket._receiver.end();
@@ -7487,11 +7496,11 @@ var require_websocket_server = __commonJS({
7487
7496
  var http = require("http");
7488
7497
  var { Duplex } = require("stream");
7489
7498
  var { createHash } = require("crypto");
7490
- var extension = require_extension();
7491
- var PerMessageDeflate = require_permessage_deflate();
7492
- var subprotocol = require_subprotocol();
7499
+ var extension2 = require_extension();
7500
+ var PerMessageDeflate2 = require_permessage_deflate();
7501
+ var subprotocol2 = require_subprotocol();
7493
7502
  var WebSocket2 = require_websocket();
7494
- var { GUID, kWebSocket } = require_constants();
7503
+ var { CLOSE_TIMEOUT, GUID, kWebSocket } = require_constants();
7495
7504
  var keyRegex = /^[+/0-9A-Za-z]{22}==$/;
7496
7505
  var RUNNING = 0;
7497
7506
  var CLOSING = 1;
@@ -7510,6 +7519,9 @@ var require_websocket_server = __commonJS({
7510
7519
  * pending connections
7511
7520
  * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
7512
7521
  * track clients
7522
+ * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
7523
+ * wait for the closing handshake to finish after `websocket.close()` is
7524
+ * called
7513
7525
  * @param {Function} [options.handleProtocols] A hook to handle protocols
7514
7526
  * @param {String} [options.host] The hostname where to bind the server
7515
7527
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
@@ -7538,6 +7550,7 @@ var require_websocket_server = __commonJS({
7538
7550
  perMessageDeflate: false,
7539
7551
  handleProtocols: null,
7540
7552
  clientTracking: true,
7553
+ closeTimeout: CLOSE_TIMEOUT,
7541
7554
  verifyClient: null,
7542
7555
  noServer: false,
7543
7556
  backlog: null,
@@ -7708,7 +7721,7 @@ var require_websocket_server = __commonJS({
7708
7721
  let protocols = /* @__PURE__ */ new Set();
7709
7722
  if (secWebSocketProtocol !== void 0) {
7710
7723
  try {
7711
- protocols = subprotocol.parse(secWebSocketProtocol);
7724
+ protocols = subprotocol2.parse(secWebSocketProtocol);
7712
7725
  } catch (err) {
7713
7726
  const message = "Invalid Sec-WebSocket-Protocol header";
7714
7727
  abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
@@ -7718,16 +7731,16 @@ var require_websocket_server = __commonJS({
7718
7731
  const secWebSocketExtensions = req.headers["sec-websocket-extensions"];
7719
7732
  const extensions = {};
7720
7733
  if (this.options.perMessageDeflate && secWebSocketExtensions !== void 0) {
7721
- const perMessageDeflate = new PerMessageDeflate(
7722
- this.options.perMessageDeflate,
7723
- true,
7724
- this.options.maxPayload
7725
- );
7734
+ const perMessageDeflate = new PerMessageDeflate2({
7735
+ ...this.options.perMessageDeflate,
7736
+ isServer: true,
7737
+ maxPayload: this.options.maxPayload
7738
+ });
7726
7739
  try {
7727
- const offers = extension.parse(secWebSocketExtensions);
7728
- if (offers[PerMessageDeflate.extensionName]) {
7729
- perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
7730
- extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
7740
+ const offers = extension2.parse(secWebSocketExtensions);
7741
+ if (offers[PerMessageDeflate2.extensionName]) {
7742
+ perMessageDeflate.accept(offers[PerMessageDeflate2.extensionName]);
7743
+ extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
7731
7744
  }
7732
7745
  } catch (err) {
7733
7746
  const message = "Invalid or unacceptable Sec-WebSocket-Extensions header";
@@ -7798,10 +7811,10 @@ var require_websocket_server = __commonJS({
7798
7811
  ws._protocol = protocol4;
7799
7812
  }
7800
7813
  }
7801
- if (extensions[PerMessageDeflate.extensionName]) {
7802
- const params = extensions[PerMessageDeflate.extensionName].params;
7803
- const value2 = extension.format({
7804
- [PerMessageDeflate.extensionName]: [params]
7814
+ if (extensions[PerMessageDeflate2.extensionName]) {
7815
+ const params = extensions[PerMessageDeflate2.extensionName].params;
7816
+ const value2 = extension2.format({
7817
+ [PerMessageDeflate2.extensionName]: [params]
7805
7818
  });
7806
7819
  headers.push(`Sec-WebSocket-Extensions: ${value2}`);
7807
7820
  ws._extensions = extensions;
@@ -7868,7 +7881,10 @@ var require_websocket_server = __commonJS({
7868
7881
  }
7869
7882
  });
7870
7883
 
7871
- // node_modules/commander/esm.mjs
7884
+ // src/cli.ts
7885
+ var import_node_dns = __toESM(require("node:dns"));
7886
+
7887
+ // ../../node_modules/commander/esm.mjs
7872
7888
  var import_index = __toESM(require_commander(), 1);
7873
7889
  var {
7874
7890
  program,
@@ -7963,10 +7979,10 @@ function configExists() {
7963
7979
  return import_fs.default.existsSync(CONFIG_FILE);
7964
7980
  }
7965
7981
 
7966
- // ../../shared/all/helpers/card-ai/claude.ts
7982
+ // ../../shared/all/helpers/board-agent-core/claude.ts
7967
7983
  var import_child_process2 = require("child_process");
7968
7984
 
7969
- // ../../shared/all/helpers/card-ai/claude-stream.ts
7985
+ // ../../shared/all/helpers/board-agent-core/claude-stream.ts
7970
7986
  function truncate(s, max) {
7971
7987
  return s.length > max ? s.slice(0, max) + "..." : s;
7972
7988
  }
@@ -8221,7 +8237,7 @@ function pipeClaudeStream(proc, callbacks) {
8221
8237
  };
8222
8238
  }
8223
8239
 
8224
- // ../../shared/all/helpers/card-ai/prompts.json
8240
+ // ../../shared/all/helpers/board-agent-core/prompts.json
8225
8241
  var prompts_default = {
8226
8242
  system: [
8227
8243
  "You are a coding agent working inside a Docker container on a task from a project management board.",
@@ -8231,7 +8247,7 @@ var prompts_default = {
8231
8247
  "2. **Implement** \u2014 Execute the plan. You gain Edit, Write, and Bash tools. Make the code changes.",
8232
8248
  "3. **Review** \u2014 Validate your changes, fix issues, generate reproduction steps for the developer.",
8233
8249
  "",
8234
- "An optional **Preview** phase may occur between Implement and Review where you set up a live dev environment.",
8250
+ "After Review, the developer may request an optional **Preview** where you set up a live dev environment on demand.",
8235
8251
  "",
8236
8252
  "## Task Tracking",
8237
8253
  "Use the TodoWrite tool to track your progress. Create todos at the start of each phase with your planned steps,",
@@ -8394,7 +8410,7 @@ var prompts_default = {
8394
8410
  commitMessage: 'Generate a single-line conventional commit message (e.g. "feat: ...", "fix: ...", "refactor: ...") for these changes. Output ONLY the commit message, nothing else.'
8395
8411
  };
8396
8412
 
8397
- // ../../shared/all/helpers/card-ai/claude.ts
8413
+ // ../../shared/all/helpers/board-agent-core/claude.ts
8398
8414
  function spawnClaude(opts) {
8399
8415
  const args = [
8400
8416
  "exec",
@@ -8494,11 +8510,18 @@ ${diff.slice(0, 8e3)}`);
8494
8510
  });
8495
8511
  }
8496
8512
 
8497
- // ../../shared/all/helpers/card-ai/docker.ts
8513
+ // ../../shared/all/helpers/board-agent-core/docker.ts
8498
8514
  var import_child_process3 = require("child_process");
8515
+ var import_crypto = __toESM(require("crypto"));
8516
+ var import_promises = __toESM(require("fs/promises"));
8499
8517
  var import_net = __toESM(require("net"));
8500
8518
  var import_util = require("util");
8501
8519
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
8520
+ var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
8521
+ async function hashDockerfile(dockerfilePath) {
8522
+ const buf = await import_promises.default.readFile(dockerfilePath);
8523
+ return import_crypto.default.createHash("sha256").update(buf).digest("hex");
8524
+ }
8502
8525
  async function getFreePort() {
8503
8526
  return new Promise((resolve, reject) => {
8504
8527
  const srv = import_net.default.createServer();
@@ -8524,28 +8547,105 @@ async function assertDockerRunning() {
8524
8547
  );
8525
8548
  }
8526
8549
  }
8527
- async function ensureDockerImage(dockerfilePath) {
8550
+ var BUILD_TIMEOUT_MS = 9e5;
8551
+ async function ensureDockerImage(dockerfilePath, onLog) {
8528
8552
  await assertDockerRunning();
8553
+ const wantedHash = await hashDockerfile(dockerfilePath);
8529
8554
  try {
8530
- await execFileAsync("docker", ["image", "inspect", "factiii-claude"], {});
8531
- return;
8555
+ const { stdout } = await execFileAsync(
8556
+ "docker",
8557
+ [
8558
+ "image",
8559
+ "inspect",
8560
+ "factiii-claude",
8561
+ "--format",
8562
+ `{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
8563
+ ],
8564
+ {}
8565
+ );
8566
+ if (stdout.trim() === wantedHash) return;
8532
8567
  } catch {
8533
8568
  }
8534
- await execFileAsync(
8535
- "docker",
8536
- ["build", "-t", "factiii-claude", "-f", dockerfilePath, "."],
8537
- { timeout: 3e5 }
8538
- );
8569
+ await buildImageStreaming(dockerfilePath, wantedHash, onLog);
8570
+ await resetNixVolume();
8539
8571
  }
8540
- async function ensureCacheVolume() {
8572
+ async function resetNixVolume() {
8541
8573
  try {
8542
- await execFileAsync(
8574
+ const { stdout } = await execFileAsync("docker", [
8575
+ "ps",
8576
+ "-aq",
8577
+ "--filter",
8578
+ "volume=factiii-nix"
8579
+ ]);
8580
+ const ids = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
8581
+ if (ids.length) await execFileAsync("docker", ["rm", "-f", ...ids]);
8582
+ await execFileAsync("docker", ["volume", "rm", "factiii-nix"]);
8583
+ } catch {
8584
+ }
8585
+ }
8586
+ function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
8587
+ return new Promise((resolve, reject) => {
8588
+ const child = (0, import_child_process3.spawn)(
8543
8589
  "docker",
8544
- ["volume", "inspect", "factiii-card-cache"],
8545
- {}
8590
+ [
8591
+ "build",
8592
+ "--progress=plain",
8593
+ "-t",
8594
+ "factiii-claude",
8595
+ "-f",
8596
+ dockerfilePath,
8597
+ "--label",
8598
+ `${DOCKERFILE_LABEL}=${wantedHash}`,
8599
+ "."
8600
+ ],
8601
+ { env: { ...process.env, DOCKER_BUILDKIT: "1" } }
8546
8602
  );
8547
- } catch {
8548
- await execFileAsync("docker", ["volume", "create", "factiii-card-cache"]);
8603
+ let timer;
8604
+ const arm = () => {
8605
+ clearTimeout(timer);
8606
+ timer = setTimeout(() => {
8607
+ child.kill("SIGKILL");
8608
+ reject(
8609
+ new Error(
8610
+ "Building container image timed out. Check Docker and your connection, then Try Again."
8611
+ )
8612
+ );
8613
+ }, BUILD_TIMEOUT_MS);
8614
+ };
8615
+ arm();
8616
+ const makeLineEmitter = () => {
8617
+ let buffer = "";
8618
+ return (buf) => {
8619
+ arm();
8620
+ buffer += buf.toString();
8621
+ const parts2 = buffer.split("\n");
8622
+ buffer = parts2.pop() ?? "";
8623
+ for (const line of parts2) {
8624
+ const trimmed = line.trimEnd();
8625
+ if (trimmed) onLog?.(trimmed);
8626
+ }
8627
+ };
8628
+ };
8629
+ child.stdout.on("data", makeLineEmitter());
8630
+ child.stderr.on("data", makeLineEmitter());
8631
+ child.on("error", (err) => {
8632
+ clearTimeout(timer);
8633
+ reject(err);
8634
+ });
8635
+ child.on("close", (code) => {
8636
+ clearTimeout(timer);
8637
+ if (code === 0) resolve();
8638
+ else reject(new Error(`docker build failed (exit code ${code}).`));
8639
+ });
8640
+ });
8641
+ }
8642
+ async function ensureCacheVolume() {
8643
+ for (const name of ["factiii-card-cache", "factiii-nix"]) {
8644
+ try {
8645
+ await execFileAsync("docker", ["volume", "inspect", name], {});
8646
+ } catch {
8647
+ await execFileAsync("docker", ["volume", "create", name]);
8648
+ }
8549
8649
  }
8550
8650
  }
8551
8651
  async function killContainer(containerName) {
@@ -8554,7 +8654,7 @@ async function killContainer(containerName) {
8554
8654
  } catch {
8555
8655
  }
8556
8656
  }
8557
- async function spawnContainer(containerName, claudeToken) {
8657
+ async function spawnContainer(containerName, claudeToken, opts) {
8558
8658
  const NUM_PORTS = 6;
8559
8659
  const ports = [];
8560
8660
  const seen = /* @__PURE__ */ new Set();
@@ -8575,10 +8675,22 @@ async function spawnContainer(containerName, claudeToken) {
8575
8675
  containerName,
8576
8676
  "--user",
8577
8677
  "claude",
8678
+ // SYS_PTRACE: gdb/strace/valgrind (Docker seccomp gates ptrace on it).
8679
+ // NET_RAW: raw-socket tools (tcpdump/nmap/ping). Scoped to the container.
8680
+ "--cap-add",
8681
+ "SYS_PTRACE",
8682
+ "--cap-add",
8683
+ "NET_RAW",
8684
+ // FUSE for rclone mount on OneDrive spaces (gated - GitHub stays unprivileged).
8685
+ ...opts?.fuse ? ["--cap-add", "SYS_ADMIN", "--device", "/dev/fuse"] : [],
8578
8686
  "-e",
8579
8687
  "HOME=/home/claude",
8688
+ ...claudeToken ? ["-e", `CLAUDE_CODE_OAUTH_TOKEN=${claudeToken}`] : [],
8689
+ // Redirect the otherwise-`~/.claude.json` config file into the .claude
8690
+ // dir, which is symlinked to /cache/claude. Keeps all claude state
8691
+ // inside one path the CLI already understands.
8580
8692
  "-e",
8581
- `CLAUDE_CODE_OAUTH_TOKEN=${claudeToken}`,
8693
+ "CLAUDE_CONFIG_DIR=/home/claude/.claude",
8582
8694
  "-e",
8583
8695
  "NODE_OPTIONS=--max-old-space-size=4096",
8584
8696
  "-e",
@@ -8597,6 +8709,8 @@ async function spawnContainer(containerName, claudeToken) {
8597
8709
  `FACTIII_PORTS=${ports.join(",")}`,
8598
8710
  "-v",
8599
8711
  "factiii-card-cache:/cache",
8712
+ "-v",
8713
+ "factiii-nix:/nix",
8600
8714
  ...portArgs,
8601
8715
  "factiii-claude",
8602
8716
  "tail",
@@ -8613,6 +8727,37 @@ async function execInContainer(containerName, cmd) {
8613
8727
  );
8614
8728
  return stdout;
8615
8729
  }
8730
+ function streamExecInContainer(containerName, cmd, onLine) {
8731
+ const child = (0, import_child_process3.spawn)("docker", [
8732
+ "exec",
8733
+ "--user",
8734
+ "claude",
8735
+ containerName,
8736
+ "sh",
8737
+ "-c",
8738
+ cmd
8739
+ ]);
8740
+ let buffer = "";
8741
+ child.stdout.on("data", (buf) => {
8742
+ buffer += buf.toString();
8743
+ const parts2 = buffer.split("\n");
8744
+ buffer = parts2.pop() ?? "";
8745
+ for (const line of parts2) {
8746
+ const trimmed = line.trimEnd();
8747
+ if (trimmed) onLine(trimmed);
8748
+ }
8749
+ });
8750
+ child.on("error", () => {
8751
+ });
8752
+ let stopped = false;
8753
+ return {
8754
+ stop: () => {
8755
+ if (stopped) return;
8756
+ stopped = true;
8757
+ child.kill("SIGKILL");
8758
+ }
8759
+ };
8760
+ }
8616
8761
  async function execInContainerAsRoot(containerName, cmd) {
8617
8762
  const { stdout } = await execFileAsync(
8618
8763
  "docker",
@@ -8624,15 +8769,19 @@ async function execInContainerAsRoot(containerName, cmd) {
8624
8769
  async function setupContainerCache(containerName) {
8625
8770
  await execInContainer(
8626
8771
  containerName,
8627
- "mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apk /cache/xdg-cache"
8772
+ "mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apt /cache/xdg-cache"
8628
8773
  );
8629
8774
  await execInContainer(
8630
8775
  containerName,
8631
8776
  "ln -sfn /cache/claude /home/claude/.claude"
8632
8777
  );
8778
+ await execInContainer(
8779
+ containerName,
8780
+ "printf 'set -g mouse on\\nset -g history-limit 50000\\nset -g set-clipboard on\\nset -ga terminal-features ,*:clipboard\\nbind -T copy-mode MouseDragEnd1Pane send -X copy-selection-and-cancel\\nbind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel\\n' > /home/claude/.tmux.conf"
8781
+ );
8633
8782
  await execInContainerAsRoot(
8634
8783
  containerName,
8635
- "ln -sfn /cache/apk /etc/apk/cache"
8784
+ "rm -rf /var/cache/apt/archives && ln -sfn /cache/apt /var/cache/apt/archives && mkdir -p /cache/apt/partial"
8636
8785
  );
8637
8786
  }
8638
8787
  async function isContainerRunning(containerName) {
@@ -8661,136 +8810,10 @@ async function getContainerPort(containerName, containerPort) {
8661
8810
  }
8662
8811
  }
8663
8812
 
8664
- // ../../shared/all/helpers/card-ai/frpc.ts
8813
+ // ../../shared/all/helpers/board-agent-core/git.ts
8665
8814
  var import_child_process4 = require("child_process");
8666
- var import_fs2 = require("fs");
8667
- var import_path2 = __toESM(require("path"));
8668
8815
  var import_util2 = require("util");
8669
-
8670
- // ../../shared/all/helpers/card-ai/scripts.ts
8671
- var FRPC_EXPOSE_SCRIPT = `#!/bin/sh
8672
- set -e
8673
- set -a; . /etc/environment.factiii 2>/dev/null || true; set +a
8674
- PORT="$1"
8675
- NAME="$2"
8676
- if [ -z "$PORT" ] || [ -z "$NAME" ]; then
8677
- echo "usage: factiii-expose <port> <name>" >&2
8678
- echo " e.g. factiii-expose 3000 app" >&2
8679
- exit 1
8680
- fi
8681
- case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
8682
- PROXY="\${FACTIII_TASK_ID}-\${NAME}"
8683
- DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
8684
- TOML="/tmp/frpc-\${NAME}.toml"
8685
- cat > "$TOML" <<EOF
8686
- serverAddr = "\${FACTIII_FRP_SERVER}"
8687
- serverPort = \${FACTIII_FRP_PORT}
8688
- transport.protocol = "kcp"
8689
-
8690
- [metadatas]
8691
- factiii_token = "\${FACTIII_FRP_TOKEN}"
8692
-
8693
- [[proxies]]
8694
- name = "\${PROXY}"
8695
- type = "http"
8696
- localPort = \${PORT}
8697
- subdomain = "\${PROXY}"
8698
- EOF
8699
- setsid frpc -c "$TOML" >/tmp/frpc-\${NAME}.log 2>&1 < /dev/null &
8700
- echo $! > /tmp/frpc-\${NAME}.pid
8701
- echo "http://\${DOMAIN}"
8702
- `;
8703
- var FRPC_UNEXPOSE_SCRIPT = `#!/bin/sh
8704
- NAME="$1"
8705
- if [ -z "$NAME" ]; then
8706
- echo "usage: factiii-unexpose <name>" >&2
8707
- exit 1
8708
- fi
8709
- PID_FILE="/tmp/frpc-\${NAME}.pid"
8710
- if [ -f "$PID_FILE" ]; then
8711
- kill "$(cat "$PID_FILE")" 2>/dev/null || true
8712
- rm -f "$PID_FILE" "/tmp/frpc-\${NAME}.toml" "/tmp/frpc-\${NAME}.log"
8713
- echo "stopped \${NAME}"
8714
- else
8715
- echo "no tunnel named \${NAME}" >&2
8716
- exit 1
8717
- fi
8718
- `;
8719
-
8720
- // ../../shared/all/helpers/card-ai/frpc.ts
8721
8816
  var execFileAsync2 = (0, import_util2.promisify)(import_child_process4.execFile);
8722
- var FRP_SERVER_ADDR = "dev.factiii.com";
8723
- var FRP_SERVER_PORT = 7443;
8724
- var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
8725
- async function writeFileAsRoot(containerName, path6, contents) {
8726
- const b64 = Buffer.from(contents, "utf-8").toString("base64");
8727
- await execInContainerAsRoot(
8728
- containerName,
8729
- `echo '${b64}' | base64 -d > '${path6}'`
8730
- );
8731
- }
8732
- async function installFrpcBinary(containerName) {
8733
- const arch = (await execInContainer(containerName, "uname -m")).trim();
8734
- const map = {
8735
- x86_64: "frpc-linux-amd64",
8736
- aarch64: "frpc-linux-arm64"
8737
- };
8738
- const binaryName = map[arch];
8739
- if (!binaryName) {
8740
- throw new Error(`Unsupported container arch for frpc: ${arch}`);
8741
- }
8742
- const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
8743
- if (!(0, import_fs2.existsSync)(src)) {
8744
- throw new Error(
8745
- `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
8746
- );
8747
- }
8748
- await execFileAsync2("docker", [
8749
- "cp",
8750
- src,
8751
- `${containerName}:/usr/local/bin/frpc`
8752
- ]);
8753
- await execInContainerAsRoot(containerName, "chmod +x /usr/local/bin/frpc");
8754
- }
8755
- async function installFrpcHarness(params) {
8756
- const { containerName, taskId, runnerToken } = params;
8757
- await installFrpcBinary(containerName);
8758
- const envLines = [
8759
- `FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
8760
- `FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
8761
- `FACTIII_FRP_TOKEN=${runnerToken}`,
8762
- `FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
8763
- `FACTIII_TASK_ID=${taskId}`
8764
- ];
8765
- await writeFileAsRoot(
8766
- containerName,
8767
- "/usr/local/bin/factiii-expose",
8768
- FRPC_EXPOSE_SCRIPT
8769
- );
8770
- await writeFileAsRoot(
8771
- containerName,
8772
- "/usr/local/bin/factiii-unexpose",
8773
- FRPC_UNEXPOSE_SCRIPT
8774
- );
8775
- await execInContainerAsRoot(
8776
- containerName,
8777
- "chmod +x /usr/local/bin/factiii-expose /usr/local/bin/factiii-unexpose"
8778
- );
8779
- await writeFileAsRoot(
8780
- containerName,
8781
- "/etc/environment.factiii",
8782
- envLines.join("\n") + "\n"
8783
- );
8784
- await execInContainerAsRoot(
8785
- containerName,
8786
- `grep -q 'environment.factiii' /home/claude/.profile 2>/dev/null || echo 'set -a; . /etc/environment.factiii; set +a' >> /home/claude/.profile && chown claude:claude /home/claude/.profile`
8787
- );
8788
- }
8789
-
8790
- // ../../shared/all/helpers/card-ai/git.ts
8791
- var import_child_process5 = require("child_process");
8792
- var import_util3 = require("util");
8793
- var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
8794
8817
  function buildCloneUrl(repoUrl, githubToken) {
8795
8818
  const url2 = new URL(repoUrl);
8796
8819
  url2.username = githubToken;
@@ -8804,7 +8827,7 @@ function repoCacheKey(repoUrl) {
8804
8827
  return repoUrl.replace(/^https?:\/\//, "").replace(/\/\/.*@/, "").replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]/g, "_");
8805
8828
  }
8806
8829
  async function gitInContainer(containerName, ...args) {
8807
- const { stdout } = await execFileAsync3(
8830
+ const { stdout } = await execFileAsync2(
8808
8831
  "docker",
8809
8832
  [
8810
8833
  "exec",
@@ -8851,7 +8874,7 @@ async function cloneRepo(containerName, cloneUrl, branch, cacheKey) {
8851
8874
  await gitInContainer(containerName, "clean", "-fd");
8852
8875
  return { fromCache: true };
8853
8876
  }
8854
- await execFileAsync3(
8877
+ await execFileAsync2(
8855
8878
  "docker",
8856
8879
  [
8857
8880
  "exec",
@@ -8894,6 +8917,7 @@ async function setGitIdentity(containerName, name, email) {
8894
8917
  );
8895
8918
  }
8896
8919
  async function extractDiff(containerName) {
8920
+ await gitInContainer(containerName, "add", "--intent-to-add", ".");
8897
8921
  const diff = await gitInContainer(containerName, "diff", "HEAD");
8898
8922
  const changedFilesRaw = (await gitInContainer(containerName, "diff", "HEAD", "--name-only")).trim();
8899
8923
  return {
@@ -8915,6 +8939,13 @@ async function listRemoteBranches(containerName) {
8915
8939
  return [];
8916
8940
  }
8917
8941
  }
8942
+ var NoChangesError = class extends Error {
8943
+ constructor(message = "No changes to commit.") {
8944
+ super(message);
8945
+ this.code = "NO_CHANGES";
8946
+ this.name = "NoChangesError";
8947
+ }
8948
+ };
8918
8949
  async function commitAndPush(containerName, branchName, commitMessage, gitConfig) {
8919
8950
  if (gitConfig?.gitName)
8920
8951
  await gitInContainer(
@@ -8930,8 +8961,8 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
8930
8961
  "user.email",
8931
8962
  gitConfig.gitEmail
8932
8963
  );
8933
- const diff = (await gitInContainer(containerName, "diff", "HEAD")).trim();
8934
- if (!diff) return;
8964
+ const status = (await gitInContainer(containerName, "status", "--porcelain")).trim();
8965
+ if (!status) throw new NoChangesError();
8935
8966
  let branchExists = false;
8936
8967
  try {
8937
8968
  await gitInContainer(
@@ -8946,7 +8977,7 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
8946
8977
  } catch {
8947
8978
  }
8948
8979
  if (branchExists) {
8949
- await gitInContainer(containerName, "stash");
8980
+ await gitInContainer(containerName, "stash", "--include-untracked");
8950
8981
  await gitInContainer(containerName, "fetch", "origin", branchName);
8951
8982
  await gitInContainer(
8952
8983
  containerName,
@@ -8962,128 +8993,273 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
8962
8993
  await gitInContainer(containerName, "add", "-A");
8963
8994
  await gitInContainer(containerName, "commit", "-m", commitMessage);
8964
8995
  await gitInContainer(containerName, "push", "origin", branchName);
8996
+ await gitInContainer(containerName, "fetch", "origin", branchName);
8997
+ const remoteSha = (await gitInContainer(containerName, "rev-parse", `origin/${branchName}`)).trim();
8998
+ const localSha = (await gitInContainer(containerName, "rev-parse", "HEAD")).trim();
8999
+ if (remoteSha !== localSha) {
9000
+ throw new Error(
9001
+ `Push verification failed: local HEAD ${localSha.slice(0, 8)} does not match origin/${branchName} ${remoteSha.slice(0, 8)}`
9002
+ );
9003
+ }
9004
+ return { pushedSha: localSha, remoteSha, branch: branchName };
8965
9005
  }
8966
9006
 
8967
- // ../../shared/all/helpers/card-ai/session.ts
8968
- function toSummary(session) {
9007
+ // ../../shared/all/helpers/board-agent-core/onedrive-auth.ts
9008
+ var DEVICECODE_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
9009
+ var TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
9010
+ var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
9011
+ var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
9012
+ var ONEDRIVE_SCOPES = "offline_access Files.ReadWrite.AppFolder User.Read";
9013
+ async function startDeviceCode(clientId) {
9014
+ const res = await fetch(DEVICECODE_URL, {
9015
+ method: "POST",
9016
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
9017
+ body: new URLSearchParams({ client_id: clientId, scope: ONEDRIVE_SCOPES })
9018
+ });
9019
+ if (!res.ok) {
9020
+ throw new Error(`OneDrive device-code start failed: ${await res.text()}`);
9021
+ }
9022
+ const data = await res.json();
8969
9023
  return {
8970
- taskId: session.taskId,
8971
- postId: session.postId,
8972
- phase: session.phase,
8973
- step: session.step,
8974
- diff: session.diff,
8975
- changedFiles: session.changedFiles,
8976
- commitMessage: session.commitMessage,
8977
- previewUrl: session.previewUrl,
8978
- reproSteps: session.reproSteps,
8979
- pendingQuestion: session.pendingQuestion,
8980
- error: session.error,
8981
- startedAt: session.startedAt,
8982
- targetBranch: session.targetBranch,
8983
- taskTitle: session.taskTitle,
8984
- todos: session.todos
9024
+ deviceCode: data.device_code,
9025
+ userCode: data.user_code,
9026
+ verificationUri: data.verification_uri,
9027
+ expiresIn: data.expires_in,
9028
+ interval: data.interval
8985
9029
  };
8986
9030
  }
8987
- function createEventSink(send, spaceSlug, notify) {
8988
- const PHASE_LABELS = {
8989
- research: "Research started",
8990
- "awaiting-input": "Input required",
8991
- implement: "Implementation started",
8992
- preview: "Preview ready",
8993
- review: "Review started",
8994
- complete: "Task complete",
8995
- error: "Task failed"
9031
+ async function refreshAccessToken(clientId, refreshToken) {
9032
+ const res = await fetch(TOKEN_URL, {
9033
+ method: "POST",
9034
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
9035
+ body: new URLSearchParams({
9036
+ grant_type: "refresh_token",
9037
+ client_id: clientId,
9038
+ refresh_token: refreshToken,
9039
+ scope: ONEDRIVE_SCOPES
9040
+ })
9041
+ });
9042
+ if (!res.ok) {
9043
+ throw new Error(`OneDrive token refresh failed: ${await res.text()}`);
9044
+ }
9045
+ const tok = await res.json();
9046
+ return {
9047
+ accessToken: tok.access_token,
9048
+ // Microsoft rotates the refresh token on each refresh; fall back to the old
9049
+ // one if a new one wasn't returned.
9050
+ refreshToken: tok.refresh_token || refreshToken,
9051
+ expiry: new Date(Date.now() + tok.expires_in * 1e3).toISOString()
8996
9052
  };
8997
- const channel = (event) => `board-agent:${spaceSlug}:${event}`;
9053
+ }
9054
+ async function pollDeviceCode(clientId, deviceCode) {
9055
+ const res = await fetch(TOKEN_URL, {
9056
+ method: "POST",
9057
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
9058
+ body: new URLSearchParams({
9059
+ grant_type: DEVICE_CODE_GRANT,
9060
+ client_id: clientId,
9061
+ device_code: deviceCode
9062
+ })
9063
+ });
9064
+ if (res.ok) {
9065
+ const tok = await res.json();
9066
+ if (!tok.refresh_token) {
9067
+ return { status: "error", message: "No refresh token returned." };
9068
+ }
9069
+ const appFolder = await getAppFolder(tok.access_token);
9070
+ return {
9071
+ status: "complete",
9072
+ refreshToken: tok.refresh_token,
9073
+ appFolder
9074
+ };
9075
+ }
9076
+ const err = await res.json().catch(() => ({}));
9077
+ switch (err.error) {
9078
+ case "authorization_pending":
9079
+ return { status: "pending" };
9080
+ case "slow_down":
9081
+ return { status: "slow_down" };
9082
+ case "expired_token":
9083
+ case "code_expired":
9084
+ return { status: "expired" };
9085
+ case "authorization_declined":
9086
+ case "access_denied":
9087
+ return { status: "declined" };
9088
+ default:
9089
+ return {
9090
+ status: "error",
9091
+ message: err.error_description || err.error || "Device-code poll failed"
9092
+ };
9093
+ }
9094
+ }
9095
+ async function getAppFolder(accessToken) {
9096
+ const res = await fetch(
9097
+ `${GRAPH_BASE}/me/drive/special/approot?$select=id,parentReference`,
9098
+ { headers: { Authorization: `Bearer ${accessToken}` } }
9099
+ );
9100
+ if (!res.ok) {
9101
+ throw new Error(
9102
+ `Failed to resolve OneDrive app folder: ${res.status} - ${await res.text()}`
9103
+ );
9104
+ }
9105
+ const data = await res.json();
9106
+ if (!data.id || !data.parentReference?.driveId) {
9107
+ throw new Error("OneDrive app folder response was incomplete.");
9108
+ }
8998
9109
  return {
8999
- emitSessionUpdate(session) {
9000
- send(channel("session-update"), toSummary(session));
9001
- const label = PHASE_LABELS[session.phase];
9002
- if (label && notify) {
9003
- notify(
9004
- `Board AI \u2014 ${label}`,
9005
- `${session.taskTitle || "Card"} (step ${session.step}/3)`,
9006
- session.phase !== "complete" && session.phase !== "error" && session.phase !== "awaiting-input"
9007
- );
9008
- }
9009
- },
9010
- emitSessionRemoved(postId) {
9011
- send(channel("session-update"), {
9012
- postId,
9013
- phase: null,
9014
- step: 1,
9015
- taskId: "",
9016
- diff: "",
9017
- changedFiles: [],
9018
- commitMessage: "",
9019
- previewUrl: "",
9020
- reproSteps: "",
9021
- pendingQuestion: "",
9022
- error: "",
9023
- startedAt: 0,
9024
- targetBranch: "",
9025
- taskTitle: "",
9026
- todos: []
9027
- });
9028
- },
9029
- emitLog(session, entry) {
9030
- send(channel(`log:${session.postId}`), entry);
9031
- },
9032
- addLog(session, type, content) {
9033
- const entry = { time: Date.now(), type, content };
9034
- session.logs.push(entry);
9035
- this.emitLog(session, entry);
9036
- },
9037
- appendTextLog(session, text) {
9038
- const last = session.logs[session.logs.length - 1];
9039
- if (last && last.type === "text") {
9040
- last.content += text;
9041
- this.emitLog(session, {
9042
- time: last.time,
9043
- type: "text-append",
9044
- content: text
9045
- });
9046
- } else {
9047
- const entry = {
9048
- time: Date.now(),
9049
- type: "text",
9050
- content: text
9051
- };
9052
- session.logs.push(entry);
9053
- this.emitLog(session, entry);
9054
- }
9055
- },
9056
- emitSetupStatus(message) {
9057
- send(channel("setup-status"), message);
9058
- }
9110
+ rootFolderId: data.id,
9111
+ driveId: data.parentReference.driveId,
9112
+ driveType: data.parentReference.driveType || "personal"
9059
9113
  };
9060
9114
  }
9061
9115
 
9062
- // ../../shared/all/helpers/card-ai/engine.ts
9063
- function buildPrompt(lines, vars = {}) {
9064
- let text = lines.join("\n");
9065
- for (const [key, value2] of Object.entries(vars)) {
9066
- text = text.replaceAll(`{{${key}}}`, value2);
9116
+ // ../../shared/all/helpers/board-agent-core/onedrive.ts
9117
+ var WORKSPACE = "/home/claude/workspace";
9118
+ var RCLONE_CONF = "/home/claude/.config/rclone/rclone.conf";
9119
+ var REMOTE = "onedrive";
9120
+ var SYNC_LOG = "/tmp/rclone-onedrive.log";
9121
+ function onedriveRemotePath(spaceSlug) {
9122
+ const safe = spaceSlug.replace(/[^a-zA-Z0-9._-]/g, "-");
9123
+ return `${REMOTE}:${safe}`;
9124
+ }
9125
+ async function writeRcloneMountConfig(containerName, conn) {
9126
+ const fresh = await refreshAccessToken(conn.clientId, conn.refreshToken);
9127
+ const tokenJson = JSON.stringify({
9128
+ access_token: fresh.accessToken,
9129
+ refresh_token: fresh.refreshToken,
9130
+ token_type: "Bearer",
9131
+ expiry: fresh.expiry
9132
+ });
9133
+ const conf = [
9134
+ `[${REMOTE}]`,
9135
+ "type = onedrive",
9136
+ `client_id = ${conn.clientId}`,
9137
+ `drive_id = ${conn.driveId}`,
9138
+ `drive_type = ${conn.driveType}`,
9139
+ `root_folder_id = ${conn.rootFolderId}`,
9140
+ // App is consumer-only, so rclone must refresh against /consumers (not the
9141
+ // default /common, which rejects it with AADSTS9002346).
9142
+ "auth_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
9143
+ "token_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
9144
+ `token = ${tokenJson}`,
9145
+ ""
9146
+ ].join("\n");
9147
+ const b64 = Buffer.from(conf, "utf-8").toString("base64");
9148
+ await execInContainer(
9149
+ containerName,
9150
+ `mkdir -p /home/claude/.config/rclone && printf %s '${b64}' | base64 -d > ${RCLONE_CONF} && chmod 600 ${RCLONE_CONF}`
9151
+ );
9152
+ return fresh.refreshToken;
9153
+ }
9154
+ async function mountWorkspace(containerName, remotePath) {
9155
+ await execInContainer(
9156
+ containerName,
9157
+ `mkdir -p ${WORKSPACE} && rclone mkdir '${remotePath}'`
9158
+ );
9159
+ const out = await execInContainer(
9160
+ containerName,
9161
+ `nohup rclone mount '${remotePath}' ${WORKSPACE} --vfs-cache-mode full --vfs-write-back 1s --dir-cache-time 30s -v --rc --rc-addr ${RC_ADDR} --rc-no-auth >> ${SYNC_LOG} 2>&1 & for i in $(seq 1 40); do mountpoint -q ${WORKSPACE} && break; sleep 0.25; done; mountpoint -q ${WORKSPACE} && echo mounted || (tail -n 20 ${SYNC_LOG} 2>/dev/null; echo mount-failed)`
9162
+ );
9163
+ if (!out.trim().endsWith("mounted")) {
9164
+ throw new Error(`OneDrive mount failed:
9165
+ ${out.trim()}`);
9067
9166
  }
9068
- return text;
9069
9167
  }
9070
- function sanitizeTaskId(taskId) {
9071
- const sanitized = String(taskId).replace(/[^a-zA-Z0-9_.-]/g, "");
9072
- if (!sanitized) throw new Error("Invalid taskId");
9073
- return sanitized;
9168
+ var syncWatchers = /* @__PURE__ */ new Map();
9169
+ var RC_ADDR = "127.0.0.1:5572";
9170
+ async function onedriveQueueNow(containerName) {
9171
+ const out = await execInContainer(
9172
+ containerName,
9173
+ `rclone rc --url http://${RC_ADDR}/ vfs/queue 2>/dev/null || echo '{}'`
9174
+ );
9175
+ try {
9176
+ const parsed = JSON.parse(out);
9177
+ return (parsed.queue ?? []).map((i) => i.name).filter((n) => !!n);
9178
+ } catch {
9179
+ return [];
9180
+ }
9181
+ }
9182
+ function startOneDriveSyncWatcher(containerName, onPending) {
9183
+ stopOneDriveSyncWatcher(containerName);
9184
+ let chain = Promise.resolve();
9185
+ const refresh = () => {
9186
+ chain = chain.then(async () => {
9187
+ const watcher = syncWatchers.get(containerName);
9188
+ if (!watcher) return;
9189
+ const files = await onedriveQueueNow(containerName);
9190
+ if (watcher.files.join("\n") !== files.join("\n")) {
9191
+ watcher.files = files;
9192
+ onPending(files.length, files);
9193
+ }
9194
+ });
9195
+ };
9196
+ const handle = streamExecInContainer(
9197
+ containerName,
9198
+ `tail -n0 -F ${SYNC_LOG} 2>/dev/null | grep --line-buffered -E 'vfs cache: (upload|starting|failed|cancel|queuing)'`,
9199
+ refresh
9200
+ );
9201
+ syncWatchers.set(containerName, { handle, files: [] });
9202
+ }
9203
+ function stopOneDriveSyncWatcher(containerName) {
9204
+ const watcher = syncWatchers.get(containerName);
9205
+ if (watcher) {
9206
+ watcher.handle.stop();
9207
+ syncWatchers.delete(containerName);
9208
+ }
9209
+ }
9210
+ function onedrivePendingUploads(containerName) {
9211
+ return syncWatchers.get(containerName)?.files.length ?? 0;
9212
+ }
9213
+ function onedriveUploadingFiles(containerName) {
9214
+ return syncWatchers.get(containerName)?.files ?? [];
9215
+ }
9216
+ async function unmountWorkspace(containerName) {
9217
+ await execInContainer(
9218
+ containerName,
9219
+ `fusermount -u ${WORKSPACE} 2>/dev/null || fusermount3 -u ${WORKSPACE} 2>/dev/null || rclone unmount ${WORKSPACE} 2>/dev/null || true`
9220
+ );
9221
+ }
9222
+ async function readbackRefreshToken(containerName) {
9223
+ let conf = "";
9224
+ try {
9225
+ conf = await execInContainer(
9226
+ containerName,
9227
+ `cat ${RCLONE_CONF} 2>/dev/null || true`
9228
+ );
9229
+ } catch {
9230
+ return null;
9231
+ }
9232
+ const match = conf.match(/token\s*=\s*(\{.*\})/);
9233
+ if (!match) return null;
9234
+ try {
9235
+ const parsed = JSON.parse(match[1]);
9236
+ return parsed.refresh_token ?? null;
9237
+ } catch {
9238
+ return null;
9239
+ }
9240
+ }
9241
+
9242
+ // ../../shared/all/helpers/board-agent-core/container.ts
9243
+ function withTimeout(work, ms, label) {
9244
+ let timer;
9245
+ const timeout = new Promise((_, reject) => {
9246
+ timer = setTimeout(
9247
+ () => reject(
9248
+ new Error(
9249
+ `${label} timed out. Check Docker and your connection, then Try Again.`
9250
+ )
9251
+ ),
9252
+ ms
9253
+ );
9254
+ });
9255
+ return Promise.race([work.finally(() => clearTimeout(timer)), timeout]);
9074
9256
  }
9075
- var BoardAgentEngine = class _BoardAgentEngine {
9257
+ var ContainerCore = class {
9076
9258
  constructor(options) {
9077
- // Utility container state
9259
+ // Utility container state.
9078
9260
  this.containerName = null;
9079
9261
  this.lastConfigKey = "";
9080
9262
  this.setupPromise = null;
9081
- // Board AI session id + active process
9082
- this.boardSessionId = null;
9083
- this.boardProcess = null;
9084
- // Board AI state
9085
- this.sessions = /* @__PURE__ */ new Map();
9086
- this.cardProcesses = /* @__PURE__ */ new Map();
9087
9263
  this.spaceSlug = options.spaceSlug;
9088
9264
  this.emitter = options.emitter;
9089
9265
  this.configProvider = options.configProvider;
@@ -9092,6 +9268,49 @@ var BoardAgentEngine = class _BoardAgentEngine {
9092
9268
  const raw = (options.instanceId || "shared").toLowerCase();
9093
9269
  this.instancePrefix = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
9094
9270
  }
9271
+ readConfig() {
9272
+ return this.configProvider.readConfig(this.spaceSlug);
9273
+ }
9274
+ isReady() {
9275
+ return this.containerName !== null;
9276
+ }
9277
+ // ── Storage backend ──
9278
+ isOneDrive(config) {
9279
+ return config.storageBackend === "onedrive";
9280
+ }
9281
+ /** Per-space OneDrive remote path inside the app folder. */
9282
+ remotePath() {
9283
+ return onedriveRemotePath(this.spaceSlug);
9284
+ }
9285
+ isOneDriveConnected() {
9286
+ return this.configProvider.readOneDriveConnection(this.spaceSlug) !== null;
9287
+ }
9288
+ /** The stored OneDrive credential, or fail with a connect hint. */
9289
+ getOneDriveConnection() {
9290
+ const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
9291
+ if (!conn) {
9292
+ throw new Error(
9293
+ "OneDrive is not connected. Connect it in Board AI settings."
9294
+ );
9295
+ }
9296
+ return conn;
9297
+ }
9298
+ saveOneDriveConnection(connection) {
9299
+ this.configProvider.writeOneDriveConnection(this.spaceSlug, connection);
9300
+ }
9301
+ clearOneDriveConnection() {
9302
+ this.configProvider.writeOneDriveConnection(this.spaceSlug, null);
9303
+ }
9304
+ /** Persist a rotated refresh token onto the existing connection. */
9305
+ updateOneDriveRefreshToken(refreshToken) {
9306
+ const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
9307
+ if (conn) {
9308
+ this.configProvider.writeOneDriveConnection(this.spaceSlug, {
9309
+ ...conn,
9310
+ refreshToken
9311
+ });
9312
+ }
9313
+ }
9095
9314
  // ── Config ──
9096
9315
  getConfig() {
9097
9316
  const config = this.configProvider.readConfig(this.spaceSlug);
@@ -9103,7 +9322,11 @@ var BoardAgentEngine = class _BoardAgentEngine {
9103
9322
  hasToken: Boolean(config.claudeToken),
9104
9323
  hasGithubToken: Boolean(config.githubToken),
9105
9324
  tokenHint: config.claudeToken ? `\xB7\xB7\xB7\xB7${config.claudeToken.slice(-4)}` : "",
9106
- githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : ""
9325
+ githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : "",
9326
+ storageBackend: config.storageBackend ?? "github",
9327
+ onedrive: {
9328
+ connected: this.isOneDriveConnected()
9329
+ }
9107
9330
  };
9108
9331
  }
9109
9332
  setConfig(partial) {
@@ -9114,7 +9337,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
9114
9337
  gitName: partial.gitName ?? existing.gitName,
9115
9338
  gitEmail: partial.gitEmail ?? existing.gitEmail,
9116
9339
  claudeToken: partial.claudeToken?.replace(/\s/g, "") || existing.claudeToken,
9117
- githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken
9340
+ githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken,
9341
+ storageBackend: partial.storageBackend ?? existing.storageBackend
9118
9342
  };
9119
9343
  this.configProvider.writeConfig(this.spaceSlug, updated);
9120
9344
  return { success: true };
@@ -9129,10 +9353,19 @@ var BoardAgentEngine = class _BoardAgentEngine {
9129
9353
  async ensureContainer(onStatus) {
9130
9354
  const config = this.configProvider.readConfig(this.spaceSlug);
9131
9355
  if (!config.claudeToken) throw new Error("No Claude token configured.");
9132
- if (!config.githubToken) throw new Error("No GitHub token configured.");
9133
- if (!config.repoUrl) throw new Error("No repository URL configured.");
9134
- const configKey = `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
9135
- const cacheKey = repoCacheKey(config.repoUrl);
9356
+ const onedrive = this.isOneDrive(config);
9357
+ if (onedrive) {
9358
+ if (!this.isOneDriveConnected()) {
9359
+ throw new Error(
9360
+ "OneDrive is not connected. Connect it in Board AI settings, then retry."
9361
+ );
9362
+ }
9363
+ } else {
9364
+ if (!config.githubToken) throw new Error("No GitHub token configured.");
9365
+ if (!config.repoUrl) throw new Error("No repository URL configured.");
9366
+ }
9367
+ const configKey = onedrive ? `onedrive|${this.spaceSlug}|${config.claudeToken}` : `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
9368
+ const cacheKey = onedrive ? `onedrive-${this.spaceSlug}`.replace(/[^a-zA-Z0-9._-]/g, "_") : repoCacheKey(config.repoUrl);
9136
9369
  const name = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
9137
9370
  if (this.containerName && this.lastConfigKey !== configKey) {
9138
9371
  await killContainer(this.containerName);
@@ -9154,16 +9387,40 @@ var BoardAgentEngine = class _BoardAgentEngine {
9154
9387
  this.emitter.emitSetupStatus(msg);
9155
9388
  };
9156
9389
  try {
9157
- status("Building Docker image...");
9158
- await ensureDockerImage(this.dockerfilePath);
9390
+ status("Step 1/4 \xB7 Checking Docker\u2026");
9391
+ await withTimeout(assertDockerRunning(), 15e3, "Checking Docker");
9392
+ status("Step 2/4 \xB7 Building container image\u2026");
9393
+ await ensureDockerImage(this.dockerfilePath, (line) => status(line));
9159
9394
  await ensureCacheVolume();
9160
9395
  await killContainer(name);
9161
- status("Starting container...");
9162
- await spawnContainer(name, config.claudeToken);
9163
- status("Cloning repository...");
9164
- const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
9165
- await cloneRepo(name, cloneUrl, config.mainBranch || "main", cacheKey);
9166
- await setGitIdentity(name, config.gitName, config.gitEmail);
9396
+ status("Step 3/4 \xB7 Starting container\u2026");
9397
+ await withTimeout(
9398
+ spawnContainer(name, config.claudeToken),
9399
+ 9e4,
9400
+ "Starting container"
9401
+ );
9402
+ if (onedrive) {
9403
+ status("Step 4/4 \xB7 Verifying OneDrive\u2026");
9404
+ const rotated = await writeRcloneMountConfig(
9405
+ name,
9406
+ this.getOneDriveConnection()
9407
+ );
9408
+ this.updateOneDriveRefreshToken(rotated);
9409
+ await withTimeout(
9410
+ execInContainer(name, `rclone lsd 'onedrive:'`),
9411
+ 6e4,
9412
+ "Verifying OneDrive"
9413
+ );
9414
+ } else {
9415
+ status("Step 4/4 \xB7 Cloning repository\u2026");
9416
+ const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
9417
+ await withTimeout(
9418
+ cloneRepo(name, cloneUrl, config.mainBranch || "main", cacheKey),
9419
+ 18e4,
9420
+ "Cloning repository"
9421
+ );
9422
+ await setGitIdentity(name, config.gitName, config.gitEmail);
9423
+ }
9167
9424
  status("");
9168
9425
  this.containerName = name;
9169
9426
  this.lastConfigKey = configKey;
@@ -9175,324 +9432,1152 @@ var BoardAgentEngine = class _BoardAgentEngine {
9175
9432
  })();
9176
9433
  return this.setupPromise;
9177
9434
  }
9178
- // ── Board AI ──
9179
- async ask(params, sendLog) {
9435
+ setupStatus() {
9436
+ if (this.containerName) return { phase: "ready" };
9437
+ if (this.setupPromise) return { phase: "running" };
9438
+ return { phase: "idle" };
9439
+ }
9440
+ async ensureSetup() {
9441
+ await this.ensureContainer();
9442
+ return { ready: true };
9443
+ }
9444
+ async cardRemoteBranches() {
9445
+ const config = this.configProvider.readConfig(this.spaceSlug);
9446
+ if (this.isOneDrive(config)) return [];
9447
+ if (!config.githubToken || !config.repoUrl) return [];
9180
9448
  if (!this.isReady()) {
9181
9449
  throw new Error(
9182
9450
  'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
9183
9451
  );
9184
9452
  }
9185
- const containerName = this.containerName;
9186
- let prompt2;
9187
- if (this.boardSessionId) {
9188
- prompt2 = params.question;
9189
- } else {
9190
- const parts2 = ["Here is the board data:", "", params.boardContext || ""];
9191
- parts2.push("", params.question);
9192
- prompt2 = parts2.join("\n");
9193
- }
9194
- const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
9195
- const { proc, result } = spawnClaude({
9196
- containerName,
9197
- prompt: prompt2,
9198
- allowedTools: "Read,Grep,Glob,Agent",
9199
- model: "claude-opus-4-6",
9200
- includePartialMessages: true,
9201
- appendSystemPrompt: systemPrompt,
9202
- resumeSessionId: this.boardSessionId || void 0,
9203
- callbacks: {
9204
- onLog: (entry) => sendLog(entry)
9205
- }
9206
- });
9207
- this.boardProcess = proc;
9208
- try {
9209
- const { text, sessionId } = await result;
9210
- if (sessionId) this.boardSessionId = sessionId;
9211
- return text;
9212
- } finally {
9213
- this.boardProcess = null;
9214
- }
9215
- }
9216
- resetBoardSession() {
9217
- if (this.boardProcess) {
9218
- try {
9219
- this.boardProcess.kill("SIGTERM");
9220
- } catch {
9221
- }
9222
- this.boardProcess = null;
9223
- }
9224
- this.boardSessionId = null;
9453
+ return listRemoteBranches(this.containerName);
9225
9454
  }
9226
- // ── Board AI: Claude runner ──
9227
- async runClaudeInContainer(session, prompt2, tools, resumeSessionId, options) {
9228
- const { proc, result } = spawnClaude({
9229
- containerName: session.containerName,
9230
- prompt: prompt2,
9231
- allowedTools: tools,
9232
- disallowedTools: options?.disallowedTools,
9233
- permissionMode: options?.permissionMode,
9234
- appendSystemPrompt: buildPrompt(prompts_default.system),
9235
- effort: session.effort,
9236
- model: "opus",
9237
- resumeSessionId,
9238
- callbacks: {
9239
- onLog: (entry) => {
9240
- if (entry.type === "text-append") {
9241
- this.emitter.appendTextLog(session, entry.content);
9242
- } else {
9243
- this.emitter.addLog(session, entry.type, entry.content);
9244
- }
9245
- },
9246
- onTodos: (todos) => {
9247
- session.todos = todos;
9248
- this.emitter.emitSessionUpdate(session);
9249
- }
9250
- }
9251
- });
9252
- this.cardProcesses.set(session.postId, proc);
9253
- try {
9254
- return await result;
9255
- } finally {
9256
- this.cardProcesses.delete(session.postId);
9455
+ destroy() {
9456
+ if (this.containerName) {
9457
+ void killContainer(this.containerName);
9458
+ this.containerName = null;
9459
+ this.setupPromise = null;
9257
9460
  }
9258
9461
  }
9259
- static {
9260
- // ── Board AI: Research with Q&A ──
9261
- this.QUESTION_MARKER = "AWAITING_INPUT";
9262
- }
9263
- async runResearch(session, prompt2, resumeSessionId) {
9264
- if (!this.sessions.has(session.postId)) return;
9265
- const { text, sessionId } = await this.runClaudeInContainer(
9462
+ };
9463
+
9464
+ // ../../shared/all/helpers/board-agent-core/runtimes.ts
9465
+ async function installRuntimes(emitter, session, attrs) {
9466
+ const clean = attrs.map((a) => a.trim()).filter((a) => a && /^[a-zA-Z0-9._-]+$/.test(a));
9467
+ if (!clean.length) return;
9468
+ emitter.addLog(
9469
+ session,
9470
+ "system",
9471
+ `Installing ${clean.join(", ")} via Nix...`
9472
+ );
9473
+ try {
9474
+ const refs = clean.map((a) => `nixpkgs#${a}`).join(" ");
9475
+ const wrapped = `nix profile install ${refs} 2>&1; echo "__FACTIII_NIX_EXIT__:$?"`;
9476
+ const out = await execInContainer(session.containerName, wrapped);
9477
+ const marker = out.lastIndexOf("__FACTIII_NIX_EXIT__:");
9478
+ const body = (marker >= 0 ? out.slice(0, marker) : out).trimEnd();
9479
+ const code = marker >= 0 ? parseInt(out.slice(marker).split(":")[1], 10) || 0 : 0;
9480
+ if (body) emitter.addLog(session, "text", body);
9481
+ emitter.addLog(
9266
9482
  session,
9267
- prompt2,
9268
- "Read,Grep,Glob,Agent,TodoWrite",
9269
- resumeSessionId,
9270
- { permissionMode: "plan", disallowedTools: "AskUserQuestion" }
9483
+ code === 0 ? "system" : "error",
9484
+ code === 0 ? "Environment ready." : `Install exited with code ${code}. Continuing \u2014 install by hand from the terminal if needed.`
9271
9485
  );
9272
- session.claudeSessionId = sessionId;
9273
- if (!this.sessions.has(session.postId)) return;
9274
- const markerIdx = text.lastIndexOf(_BoardAgentEngine.QUESTION_MARKER);
9275
- if (markerIdx === -1) return;
9276
- session.phase = "awaiting-input";
9277
- session.pendingQuestion = text.slice(markerIdx + _BoardAgentEngine.QUESTION_MARKER.length).trim();
9278
- this.emitter.emitSessionUpdate(session);
9279
- this.emitter.addLog(
9486
+ } catch (err) {
9487
+ emitter.addLog(
9280
9488
  session,
9281
- "system",
9282
- "Claude has a question \u2014 waiting for your input."
9489
+ "error",
9490
+ `Install failed: ${err instanceof Error ? err.message : String(err)}`
9283
9491
  );
9284
- const answer = await new Promise((resolve) => {
9285
- session.answerResolver = resolve;
9286
- });
9287
- if (!this.sessions.has(session.postId)) return;
9288
- session.phase = "research";
9289
- session.pendingQuestion = "";
9290
- session.answerResolver = null;
9291
- this.emitter.emitSessionUpdate(session);
9292
- this.emitter.addLog(session, "system", "Resuming research...");
9293
- return this.runResearch(session, answer, sessionId);
9294
9492
  }
9295
- // ── Board AI: Pipeline ──
9296
- async runPipeline(session, prompt2, enablePreview, baseBranch) {
9297
- if (!this.isReady()) {
9298
- throw new Error(
9299
- 'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
9300
- );
9301
- }
9302
- const config = this.configProvider.readConfig(this.spaceSlug);
9303
- this.emitter.addLog(session, "system", "Starting container...");
9304
- const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
9305
- const branch = baseBranch || config.mainBranch || "main";
9306
- const cacheKey = repoCacheKey(config.repoUrl);
9307
- await killContainer(session.containerName);
9308
- await spawnContainer(session.containerName, config.claudeToken);
9309
- const { fromCache } = await cloneRepo(
9310
- session.containerName,
9311
- cloneUrl,
9312
- branch,
9313
- cacheKey
9314
- );
9315
- this.emitter.addLog(
9316
- session,
9317
- "system",
9318
- fromCache ? "Repository updated from cache." : "Repository cloned."
9319
- );
9320
- await setGitIdentity(
9321
- session.containerName,
9322
- config.gitName,
9323
- config.gitEmail
9324
- );
9325
- this.emitter.addLog(session, "system", "Container ready.");
9326
- session.phase = "research";
9327
- session.step = 1;
9328
- this.emitter.emitSessionUpdate(session);
9329
- this.emitter.addLog(session, "system", "\u2500\u2500 Step 1: Research \u2500\u2500");
9330
- await this.runResearch(
9331
- session,
9493
+ }
9494
+
9495
+ // ../../shared/all/helpers/board-agent-core/session.ts
9496
+ function toSummary(session) {
9497
+ return {
9498
+ taskId: session.taskId,
9499
+ postId: session.postId,
9500
+ phase: session.phase,
9501
+ mode: session.mode,
9502
+ step: session.step,
9503
+ diff: session.diff,
9504
+ changedFiles: session.changedFiles,
9505
+ commitMessage: session.commitMessage,
9506
+ previewUrl: session.previewUrl,
9507
+ previewRunning: session.previewRunning,
9508
+ reproSteps: session.reproSteps,
9509
+ pendingQuestion: session.pendingQuestion,
9510
+ error: session.error,
9511
+ startedAt: session.startedAt,
9512
+ targetBranch: session.targetBranch,
9513
+ taskTitle: session.taskTitle,
9514
+ todos: session.todos
9515
+ };
9516
+ }
9517
+ function createEventSink(send, spaceSlug, notify) {
9518
+ const PHASE_LABELS = {
9519
+ research: "Research started",
9520
+ "awaiting-input": "Input required",
9521
+ implement: "Implementation started",
9522
+ review: "Review started",
9523
+ complete: "Task complete",
9524
+ error: "Task failed"
9525
+ };
9526
+ const channel = (event) => `board-agent:${spaceSlug}:${event}`;
9527
+ return {
9528
+ emitSessionUpdate(session) {
9529
+ send(channel("session-update"), toSummary(session));
9530
+ const label = PHASE_LABELS[session.phase];
9531
+ if (label && notify) {
9532
+ notify(
9533
+ `Board AI - ${label}`,
9534
+ `${session.taskTitle || "Card"} (step ${session.step}/3)`,
9535
+ session.phase !== "complete" && session.phase !== "error" && session.phase !== "awaiting-input"
9536
+ );
9537
+ }
9538
+ },
9539
+ emitSessionRemoved(postId) {
9540
+ send(channel("session-update"), {
9541
+ postId,
9542
+ phase: null,
9543
+ mode: "claude",
9544
+ step: 1,
9545
+ taskId: "",
9546
+ diff: "",
9547
+ changedFiles: [],
9548
+ commitMessage: "",
9549
+ previewUrl: "",
9550
+ previewRunning: false,
9551
+ reproSteps: "",
9552
+ pendingQuestion: "",
9553
+ error: "",
9554
+ startedAt: 0,
9555
+ targetBranch: "",
9556
+ taskTitle: "",
9557
+ todos: []
9558
+ });
9559
+ },
9560
+ emitLog(session, entry) {
9561
+ send(channel(`log:${session.postId}`), entry);
9562
+ },
9563
+ addLog(session, type, content) {
9564
+ const entry = { time: Date.now(), type, content };
9565
+ session.logs.push(entry);
9566
+ this.emitLog(session, entry);
9567
+ },
9568
+ appendTextLog(session, text) {
9569
+ const last = session.logs[session.logs.length - 1];
9570
+ if (last && last.type === "text") {
9571
+ last.content += text;
9572
+ this.emitLog(session, {
9573
+ time: last.time,
9574
+ type: "text-append",
9575
+ content: text
9576
+ });
9577
+ } else {
9578
+ const entry = {
9579
+ time: Date.now(),
9580
+ type: "text",
9581
+ content: text
9582
+ };
9583
+ session.logs.push(entry);
9584
+ this.emitLog(session, entry);
9585
+ }
9586
+ },
9587
+ emitSetupStatus(message) {
9588
+ send(channel("setup-status"), message);
9589
+ },
9590
+ emitOneDriveAuth(payload) {
9591
+ send(channel("onedrive-auth"), payload);
9592
+ },
9593
+ emitOneDriveSync(payload) {
9594
+ send(channel("onedrive-sync"), payload);
9595
+ }
9596
+ };
9597
+ }
9598
+
9599
+ // ../../shared/all/helpers/card-ai/frpc.ts
9600
+ var import_child_process5 = require("child_process");
9601
+ var import_fs2 = require("fs");
9602
+ var import_path2 = __toESM(require("path"));
9603
+ var import_util3 = require("util");
9604
+
9605
+ // ../../shared/all/helpers/card-ai/scripts.ts
9606
+ var FRPC_EXPOSE_SCRIPT = `#!/bin/sh
9607
+ set -e
9608
+ set -a; . /etc/environment.factiii 2>/dev/null || true; set +a
9609
+ PORT="$1"
9610
+ NAME="$2"
9611
+ if [ -z "$PORT" ] || [ -z "$NAME" ]; then
9612
+ echo "usage: factiii-expose <port> <name>" >&2
9613
+ echo " e.g. factiii-expose 3000 app" >&2
9614
+ exit 1
9615
+ fi
9616
+ case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
9617
+ PROXY="\${FACTIII_TASK_ID}-\${NAME}"
9618
+ DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
9619
+ TOML="/tmp/frpc-\${NAME}.toml"
9620
+ cat > "$TOML" <<EOF
9621
+ serverAddr = "\${FACTIII_FRP_SERVER}"
9622
+ serverPort = \${FACTIII_FRP_PORT}
9623
+ transport.protocol = "tcp"
9624
+
9625
+ [metadatas]
9626
+ factiii_token = "\${FACTIII_FRP_TOKEN}"
9627
+
9628
+ [[proxies]]
9629
+ name = "\${PROXY}"
9630
+ type = "http"
9631
+ localPort = \${PORT}
9632
+ subdomain = "\${PROXY}"
9633
+ EOF
9634
+ setsid frpc -c "$TOML" >/tmp/frpc-\${NAME}.log 2>&1 < /dev/null &
9635
+ echo $! > /tmp/frpc-\${NAME}.pid
9636
+ echo "http://\${DOMAIN}"
9637
+ `;
9638
+ var FRPC_UNEXPOSE_SCRIPT = `#!/bin/sh
9639
+ NAME="$1"
9640
+ if [ -z "$NAME" ]; then
9641
+ echo "usage: factiii-unexpose <name>" >&2
9642
+ exit 1
9643
+ fi
9644
+ PID_FILE="/tmp/frpc-\${NAME}.pid"
9645
+ if [ -f "$PID_FILE" ]; then
9646
+ kill "$(cat "$PID_FILE")" 2>/dev/null || true
9647
+ rm -f "$PID_FILE" "/tmp/frpc-\${NAME}.toml" "/tmp/frpc-\${NAME}.log"
9648
+ echo "stopped \${NAME}"
9649
+ else
9650
+ echo "no tunnel named \${NAME}" >&2
9651
+ exit 1
9652
+ fi
9653
+ `;
9654
+
9655
+ // ../../shared/all/helpers/card-ai/frpc.ts
9656
+ var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
9657
+ var FRP_SERVER_ADDR = "dev.factiii.com";
9658
+ var FRP_SERVER_PORT = 7443;
9659
+ var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
9660
+ async function writeFileAsRoot(containerName, path7, contents) {
9661
+ const b64 = Buffer.from(contents, "utf-8").toString("base64");
9662
+ await execInContainerAsRoot(
9663
+ containerName,
9664
+ `echo '${b64}' | base64 -d > '${path7}'`
9665
+ );
9666
+ }
9667
+ async function installFrpcBinary(containerName) {
9668
+ const arch = (await execInContainer(containerName, "uname -m")).trim();
9669
+ const map = {
9670
+ x86_64: "frpc-linux-amd64",
9671
+ aarch64: "frpc-linux-arm64"
9672
+ };
9673
+ const binaryName = map[arch];
9674
+ if (!binaryName) {
9675
+ throw new Error(`Unsupported container arch for frpc: ${arch}`);
9676
+ }
9677
+ const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
9678
+ if (!(0, import_fs2.existsSync)(src)) {
9679
+ throw new Error(
9680
+ `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
9681
+ );
9682
+ }
9683
+ await execFileAsync3("docker", [
9684
+ "cp",
9685
+ src,
9686
+ `${containerName}:/usr/local/bin/frpc`
9687
+ ]);
9688
+ await execInContainerAsRoot(containerName, "chmod +x /usr/local/bin/frpc");
9689
+ }
9690
+ async function installFrpcHarness(params) {
9691
+ const { containerName, taskId, runnerToken } = params;
9692
+ await installFrpcBinary(containerName);
9693
+ const envLines = [
9694
+ `FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
9695
+ `FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
9696
+ `FACTIII_FRP_TOKEN=${runnerToken}`,
9697
+ `FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
9698
+ `FACTIII_TASK_ID=${taskId}`
9699
+ ];
9700
+ await writeFileAsRoot(
9701
+ containerName,
9702
+ "/usr/local/bin/factiii-expose",
9703
+ FRPC_EXPOSE_SCRIPT
9704
+ );
9705
+ await writeFileAsRoot(
9706
+ containerName,
9707
+ "/usr/local/bin/factiii-unexpose",
9708
+ FRPC_UNEXPOSE_SCRIPT
9709
+ );
9710
+ await execInContainerAsRoot(
9711
+ containerName,
9712
+ "chmod +x /usr/local/bin/factiii-expose /usr/local/bin/factiii-unexpose"
9713
+ );
9714
+ await writeFileAsRoot(
9715
+ containerName,
9716
+ "/etc/environment.factiii",
9717
+ envLines.join("\n") + "\n"
9718
+ );
9719
+ await execInContainerAsRoot(
9720
+ containerName,
9721
+ `grep -q 'environment.factiii' /home/claude/.profile 2>/dev/null || echo 'set -a; . /etc/environment.factiii; set +a' >> /home/claude/.profile && chown claude:claude /home/claude/.profile`
9722
+ );
9723
+ }
9724
+
9725
+ // ../../shared/all/helpers/card-ai/claude-mode.ts
9726
+ function buildPrompt(lines, vars = {}) {
9727
+ let text = lines.join("\n");
9728
+ for (const [key, value2] of Object.entries(vars)) {
9729
+ text = text.replaceAll(`{{${key}}}`, value2);
9730
+ }
9731
+ return text;
9732
+ }
9733
+ var ONEDRIVE_PROMPT_SWAPS = [
9734
+ [
9735
+ "- Full clone with complete git history available.",
9736
+ "- Your file changes are saved automatically when the task finishes."
9737
+ ],
9738
+ [
9739
+ "1. Review the diff of your changes (git diff HEAD)",
9740
+ "1. Review the changes you made"
9741
+ ]
9742
+ ];
9743
+ function gitFree(text) {
9744
+ let out = text;
9745
+ for (const [from, to] of ONEDRIVE_PROMPT_SWAPS) out = out.replace(from, to);
9746
+ return out;
9747
+ }
9748
+ var ClaudeModeEngine = class _ClaudeModeEngine {
9749
+ constructor(core, emitter) {
9750
+ this.core = core;
9751
+ this.emitter = emitter;
9752
+ this.sessions = /* @__PURE__ */ new Map();
9753
+ this.board = { sessionId: null, process: null, messages: [] };
9754
+ }
9755
+ static {
9756
+ this.QUESTION_MARKER = "AWAITING_INPUT";
9757
+ }
9758
+ static {
9759
+ this.FULL_TOOLS = "Read,Grep,Glob,Agent,Edit,Write,Bash,TodoWrite";
9760
+ }
9761
+ // ── Board AI ("Ask AI") ──
9762
+ async ask(params, sendLog) {
9763
+ if (!this.core.isReady()) {
9764
+ throw new Error(
9765
+ 'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
9766
+ );
9767
+ }
9768
+ const containerName = this.core.containerName;
9769
+ this.board.messages.push({ role: "user", content: params.question });
9770
+ let prompt2;
9771
+ if (this.board.sessionId) {
9772
+ prompt2 = params.question;
9773
+ } else {
9774
+ const parts2 = ["Here is the board data:", "", params.boardContext || ""];
9775
+ parts2.push("", params.question);
9776
+ prompt2 = parts2.join("\n");
9777
+ }
9778
+ const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
9779
+ const { proc, result } = spawnClaude({
9780
+ containerName,
9781
+ prompt: prompt2,
9782
+ allowedTools: "Read,Grep,Glob,Agent",
9783
+ model: "claude-opus-4-6",
9784
+ includePartialMessages: true,
9785
+ appendSystemPrompt: systemPrompt,
9786
+ resumeSessionId: this.board.sessionId || void 0,
9787
+ callbacks: {
9788
+ onLog: (entry) => sendLog(entry)
9789
+ }
9790
+ });
9791
+ this.board.process = proc;
9792
+ try {
9793
+ const { text, sessionId } = await result;
9794
+ if (sessionId) this.board.sessionId = sessionId;
9795
+ this.board.messages.push({ role: "assistant", content: text });
9796
+ return text;
9797
+ } finally {
9798
+ this.board.process = null;
9799
+ }
9800
+ }
9801
+ /** The persisted Ask AI conversation, for the client to hydrate on open. */
9802
+ boardSession() {
9803
+ return { messages: this.board.messages.slice() };
9804
+ }
9805
+ resetBoardSession() {
9806
+ if (this.board.process) {
9807
+ try {
9808
+ this.board.process.kill("SIGTERM");
9809
+ } catch {
9810
+ }
9811
+ }
9812
+ this.board = { sessionId: null, process: null, messages: [] };
9813
+ }
9814
+ // ── Claude runner ──
9815
+ /** Strip git mentions from an agent prompt on OneDrive spaces; pass-through otherwise. */
9816
+ agentPrompt(text) {
9817
+ return this.core.isOneDrive(this.core.readConfig()) ? gitFree(text) : text;
9818
+ }
9819
+ async runClaudeInContainer(session, prompt2, tools, resumeSessionId, options) {
9820
+ const { proc, result } = spawnClaude({
9821
+ containerName: session.containerName,
9822
+ prompt: prompt2,
9823
+ allowedTools: tools,
9824
+ disallowedTools: options?.disallowedTools,
9825
+ permissionMode: options?.permissionMode,
9826
+ appendSystemPrompt: this.agentPrompt(buildPrompt(prompts_default.system)),
9827
+ effort: session.effort,
9828
+ model: "opus",
9829
+ resumeSessionId,
9830
+ callbacks: {
9831
+ onLog: (entry) => {
9832
+ if (entry.type === "text-append") {
9833
+ this.emitter.appendTextLog(session, entry.content);
9834
+ } else {
9835
+ this.emitter.addLog(session, entry.type, entry.content);
9836
+ }
9837
+ },
9838
+ onTodos: (todos) => {
9839
+ session.todos = todos;
9840
+ this.emitter.emitSessionUpdate(session);
9841
+ }
9842
+ }
9843
+ });
9844
+ session.proc = proc;
9845
+ try {
9846
+ return await result;
9847
+ } finally {
9848
+ session.proc = null;
9849
+ }
9850
+ }
9851
+ // ── Research with Q&A ──
9852
+ async runResearch(session, prompt2, resumeSessionId) {
9853
+ if (!this.sessions.has(session.postId)) return;
9854
+ const { text, sessionId } = await this.runClaudeInContainer(
9855
+ session,
9856
+ prompt2,
9857
+ "Read,Grep,Glob,Agent,TodoWrite",
9858
+ resumeSessionId,
9859
+ { permissionMode: "plan", disallowedTools: "AskUserQuestion" }
9860
+ );
9861
+ session.claudeSessionId = sessionId;
9862
+ if (!this.sessions.has(session.postId)) return;
9863
+ const markerIdx = text.lastIndexOf(_ClaudeModeEngine.QUESTION_MARKER);
9864
+ if (markerIdx === -1) return;
9865
+ session.phase = "awaiting-input";
9866
+ session.pendingQuestion = text.slice(markerIdx + _ClaudeModeEngine.QUESTION_MARKER.length).trim();
9867
+ this.emitter.emitSessionUpdate(session);
9868
+ this.emitter.addLog(
9869
+ session,
9870
+ "system",
9871
+ "Claude has a question - waiting for your input."
9872
+ );
9873
+ const answer = await new Promise((resolve) => {
9874
+ session.answerResolver = resolve;
9875
+ });
9876
+ if (!this.sessions.has(session.postId)) return;
9877
+ session.phase = "research";
9878
+ session.pendingQuestion = "";
9879
+ session.answerResolver = null;
9880
+ this.emitter.emitSessionUpdate(session);
9881
+ this.emitter.addLog(session, "system", "Resuming research...");
9882
+ return this.runResearch(session, answer, sessionId);
9883
+ }
9884
+ // ── Pipeline ──
9885
+ async runPipeline(session, prompt2, baseBranch, runtimes) {
9886
+ if (!this.core.isReady()) {
9887
+ throw new Error(
9888
+ 'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
9889
+ );
9890
+ }
9891
+ const config = this.core.readConfig();
9892
+ this.emitter.addLog(session, "system", "Starting container...");
9893
+ const onedrive = this.core.isOneDrive(config);
9894
+ await killContainer(session.containerName);
9895
+ await spawnContainer(session.containerName, config.claudeToken, {
9896
+ fuse: onedrive
9897
+ });
9898
+ if (onedrive) {
9899
+ const rotated = await writeRcloneMountConfig(
9900
+ session.containerName,
9901
+ this.core.getOneDriveConnection()
9902
+ );
9903
+ this.core.updateOneDriveRefreshToken(rotated);
9904
+ await mountWorkspace(session.containerName, this.core.remotePath());
9905
+ startOneDriveSyncWatcher(
9906
+ session.containerName,
9907
+ (pending, files) => this.emitter.emitOneDriveSync({
9908
+ postId: session.postId,
9909
+ pending,
9910
+ files
9911
+ })
9912
+ );
9913
+ this.emitter.addLog(session, "system", "OneDrive folder mounted.");
9914
+ } else {
9915
+ const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
9916
+ const branch = baseBranch || config.mainBranch || "main";
9917
+ const cacheKey = repoCacheKey(config.repoUrl);
9918
+ const { fromCache } = await cloneRepo(
9919
+ session.containerName,
9920
+ cloneUrl,
9921
+ branch,
9922
+ cacheKey
9923
+ );
9924
+ this.emitter.addLog(
9925
+ session,
9926
+ "system",
9927
+ fromCache ? "Repository updated from cache." : "Repository cloned."
9928
+ );
9929
+ await setGitIdentity(
9930
+ session.containerName,
9931
+ config.gitName,
9932
+ config.gitEmail
9933
+ );
9934
+ }
9935
+ await installRuntimes(this.emitter, session, runtimes);
9936
+ this.emitter.addLog(session, "system", "Container ready.");
9937
+ session.phase = "research";
9938
+ session.step = 1;
9939
+ this.emitter.emitSessionUpdate(session);
9940
+ this.emitter.addLog(session, "system", "\u2500\u2500 Step 1: Research \u2500\u2500");
9941
+ await this.runResearch(
9942
+ session,
9332
9943
  buildPrompt(prompts_default.research, { task: prompt2 })
9333
9944
  );
9334
9945
  if (!this.sessions.has(session.postId)) return;
9335
9946
  await this.runImplementation(
9336
9947
  session,
9337
9948
  buildPrompt(prompts_default.implement),
9338
- "\u2500\u2500 Step",
9339
- enablePreview
9949
+ "\u2500\u2500 Step"
9950
+ );
9951
+ }
9952
+ async runImplementation(session, implementPrompt, logPrefix) {
9953
+ const tools = _ClaudeModeEngine.FULL_TOOLS;
9954
+ session.phase = "implement";
9955
+ session.step = 2;
9956
+ this.emitter.emitSessionUpdate(session);
9957
+ this.emitter.addLog(session, "system", `${logPrefix} 2: Implement \u2500\u2500`);
9958
+ const implResult = await this.runClaudeInContainer(
9959
+ session,
9960
+ implementPrompt,
9961
+ tools,
9962
+ session.claudeSessionId
9963
+ );
9964
+ session.claudeSessionId = implResult.sessionId || session.claudeSessionId;
9965
+ if (!this.sessions.has(session.postId)) return;
9966
+ session.phase = "review";
9967
+ session.step = 3;
9968
+ this.emitter.emitSessionUpdate(session);
9969
+ this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
9970
+ const reviewResult = await this.runClaudeInContainer(
9971
+ session,
9972
+ this.agentPrompt(buildPrompt(prompts_default.review)),
9973
+ tools,
9974
+ session.claudeSessionId
9340
9975
  );
9976
+ session.claudeSessionId = reviewResult.sessionId || session.claudeSessionId;
9977
+ if (!this.sessions.has(session.postId)) return;
9978
+ session.reproSteps = reviewResult.text.trim();
9979
+ await this.extractAndComplete(session);
9980
+ }
9981
+ // On-demand dev-server preview: reuses the live container + Claude session,
9982
+ // maps the port (or a public frpc tunnel), stores the URL. Stays in `complete`.
9983
+ async runPreview(session) {
9984
+ const tools = _ClaudeModeEngine.FULL_TOOLS;
9985
+ session.previewRunning = true;
9986
+ this.emitter.addLog(session, "system", "Setting up live preview...");
9987
+ this.emitter.emitSessionUpdate(session);
9988
+ const portsEnv = (await execInContainer(session.containerName, "echo -n $FACTIII_PORTS")).trim();
9989
+ const publicPreviewEnabled = !!this.core.runnerToken;
9990
+ if (publicPreviewEnabled && this.core.runnerToken) {
9991
+ await installFrpcHarness({
9992
+ containerName: session.containerName,
9993
+ taskId: session.taskId,
9994
+ runnerToken: this.core.runnerToken
9995
+ });
9996
+ }
9997
+ const previewResult = await this.runClaudeInContainer(
9998
+ session,
9999
+ buildPrompt(prompts_default.preview, {
10000
+ ports: portsEnv,
10001
+ publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
10002
+ }),
10003
+ tools,
10004
+ session.claudeSessionId
10005
+ );
10006
+ session.claudeSessionId = previewResult.sessionId || session.claudeSessionId;
10007
+ if (!this.sessions.has(session.postId)) return;
10008
+ const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
10009
+ const previewPort = portMatch ? portMatch[1] : "3000";
10010
+ const urlMatch = previewResult.text.match(/PREVIEW_URL:\s*(\S+)/);
10011
+ if (publicPreviewEnabled && urlMatch) {
10012
+ session.previewUrl = urlMatch[1];
10013
+ this.emitter.addLog(
10014
+ session,
10015
+ "system",
10016
+ `Public preview available at ${session.previewUrl}`
10017
+ );
10018
+ } else {
10019
+ const previewUrl = await getContainerPort(
10020
+ session.containerName,
10021
+ previewPort
10022
+ );
10023
+ if (previewUrl) {
10024
+ session.previewUrl = previewUrl;
10025
+ this.emitter.addLog(
10026
+ session,
10027
+ "system",
10028
+ `Preview available at ${session.previewUrl}`
10029
+ );
10030
+ } else {
10031
+ this.emitter.addLog(
10032
+ session,
10033
+ "system",
10034
+ "Could not map preview port. Server may not be running."
10035
+ );
10036
+ }
10037
+ }
10038
+ this.emitter.emitSessionUpdate(session);
9341
10039
  }
9342
- // ── Board AI: Handlers ──
9343
- async cardExecute({
9344
- postId: rawPostId,
9345
- prompt: prompt2,
9346
- enablePreview,
9347
- baseBranch,
9348
- targetBranch,
9349
- taskTitle,
9350
- effort
9351
- }) {
9352
- const postId = String(rawPostId);
9353
- const existing = this.sessions.get(postId);
9354
- if (existing && (existing.phase === "starting" || existing.phase === "research" || existing.phase === "implement")) {
9355
- throw new Error("A session is already running for this card.");
10040
+ async extractAndComplete(session) {
10041
+ const config = this.core.readConfig();
10042
+ if (this.core.isOneDrive(config)) {
10043
+ session.diff = "";
10044
+ session.changedFiles = [];
10045
+ session.commitMessage = "";
10046
+ session.phase = "complete";
10047
+ this.emitter.emitSessionUpdate(session);
10048
+ return;
9356
10049
  }
9357
- if (existing) {
9358
- await killContainer(existing.containerName);
9359
- this.sessions.delete(postId);
10050
+ this.emitter.addLog(session, "system", "Extracting changes...");
10051
+ const { diff, changedFiles } = await extractDiff(session.containerName);
10052
+ session.diff = diff;
10053
+ session.changedFiles = changedFiles;
10054
+ if (changedFiles.length > 0) {
10055
+ this.emitter.addLog(
10056
+ session,
10057
+ "system",
10058
+ `${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
10059
+ );
10060
+ this.emitter.addLog(session, "system", "Generating commit message...");
10061
+ session.commitMessage = await generateCommitMessage(
10062
+ session.containerName,
10063
+ diff
10064
+ );
10065
+ } else {
10066
+ this.emitter.addLog(session, "system", "No files changed.");
9360
10067
  }
9361
- const taskId = sanitizeTaskId(`${postId}-${Date.now()}`);
10068
+ session.phase = "complete";
10069
+ this.emitter.emitSessionUpdate(session);
10070
+ }
10071
+ // ── Lifecycle / ModeEngine ──
10072
+ start(base, args) {
9362
10073
  const session = {
9363
- taskId,
9364
- postId,
9365
- phase: "starting",
9366
- step: 1,
9367
- logs: [],
9368
- diff: "",
9369
- changedFiles: [],
9370
- commitMessage: "",
9371
- previewUrl: "",
9372
- reproSteps: "",
9373
- pendingQuestion: "",
9374
- error: "",
9375
- startedAt: Date.now(),
9376
- taskTitle: taskTitle || "",
9377
- targetBranch: targetBranch || "",
9378
- effort: effort || "high",
9379
- todos: [],
9380
- containerName: `factiii-${this.instancePrefix}-card-${taskId}`,
10074
+ ...base,
9381
10075
  claudeSessionId: "",
9382
10076
  answerResolver: null,
9383
- previewResolver: null
10077
+ proc: null
9384
10078
  };
9385
- this.sessions.set(postId, session);
10079
+ this.sessions.set(session.postId, session);
9386
10080
  this.emitter.emitSessionUpdate(session);
10081
+ const onError = (err) => {
10082
+ if (!this.sessions.has(session.postId)) return;
10083
+ session.phase = "error";
10084
+ session.error = err instanceof Error ? err.message : String(err);
10085
+ this.emitter.addLog(session, "error", session.error);
10086
+ this.emitter.emitSessionUpdate(session);
10087
+ };
9387
10088
  this.runPipeline(
9388
10089
  session,
9389
- prompt2,
9390
- enablePreview ?? false,
9391
- baseBranch ?? ""
9392
- ).catch((err) => {
10090
+ args.prompt,
10091
+ args.baseBranch,
10092
+ args.runtimes
10093
+ ).catch(onError);
10094
+ }
10095
+ // Triggered from the review (complete) phase; returns the live preview URL.
10096
+ async cardStartPreview({
10097
+ postId: rawPostId
10098
+ }) {
10099
+ const postId = String(rawPostId);
10100
+ const session = this.sessions.get(postId);
10101
+ if (!session) throw new Error("No active session for this card.");
10102
+ if (session.phase !== "complete")
10103
+ throw new Error("Preview is only available once changes are ready.");
10104
+ if (!session.claudeSessionId)
10105
+ throw new Error("No Claude session to resume for preview.");
10106
+ if (session.previewRunning || session.proc)
10107
+ return { previewUrl: session.previewUrl };
10108
+ try {
10109
+ await this.runPreview(session);
10110
+ } finally {
10111
+ session.previewRunning = false;
10112
+ if (this.sessions.has(postId)) this.emitter.emitSessionUpdate(session);
10113
+ }
10114
+ return { previewUrl: session.previewUrl };
10115
+ }
10116
+ cardAnswerQuestion({
10117
+ postId: rawPostId,
10118
+ answer
10119
+ }) {
10120
+ const postId = String(rawPostId);
10121
+ const session = this.sessions.get(postId);
10122
+ if (!session || session.phase !== "awaiting-input" || !session.answerResolver)
10123
+ return;
10124
+ session.answerResolver(answer);
10125
+ }
10126
+ cardFeedback({
10127
+ postId: rawPostId,
10128
+ feedback
10129
+ }) {
10130
+ const postId = String(rawPostId);
10131
+ const session = this.sessions.get(postId);
10132
+ if (!session || session.phase !== "complete") return;
10133
+ if (!session.claudeSessionId) return;
10134
+ session.diff = "";
10135
+ session.changedFiles = [];
10136
+ session.commitMessage = "";
10137
+ session.reproSteps = "";
10138
+ const prompt2 = `The user reviewed your changes and has the following feedback:
10139
+
10140
+ ${feedback}
10141
+
10142
+ ${buildPrompt(prompts_default.implement)}`;
10143
+ this.runImplementation(session, prompt2, "\u2500\u2500 Follow-up").catch((err) => {
9393
10144
  if (!this.sessions.has(postId)) return;
9394
10145
  session.phase = "error";
9395
10146
  session.error = err instanceof Error ? err.message : String(err);
9396
10147
  this.emitter.addLog(session, "error", session.error);
9397
10148
  this.emitter.emitSessionUpdate(session);
9398
10149
  });
9399
- return { taskId, postId };
9400
10150
  }
9401
- static {
9402
- this.FULL_TOOLS = "Read,Grep,Glob,Agent,Edit,Write,Bash,TodoWrite";
10151
+ listSummaries() {
10152
+ const result = {};
10153
+ for (const [postId, session] of this.sessions) {
10154
+ result[postId] = toSummary(session);
10155
+ }
10156
+ return result;
9403
10157
  }
9404
- async runImplementation(session, implementPrompt, logPrefix, enablePreview = false) {
9405
- const tools = _BoardAgentEngine.FULL_TOOLS;
9406
- session.phase = "implement";
9407
- session.step = 2;
10158
+ get(postId) {
10159
+ return this.sessions.get(postId);
10160
+ }
10161
+ sessionWithLogs(postId) {
10162
+ const session = this.sessions.get(postId);
10163
+ if (!session) return null;
10164
+ return { ...toSummary(session), logs: session.logs };
10165
+ }
10166
+ has(postId) {
10167
+ return this.sessions.has(postId);
10168
+ }
10169
+ isRunning(postId) {
10170
+ const s = this.sessions.get(postId);
10171
+ return !!s && (s.phase === "starting" || s.phase === "research" || s.phase === "implement");
10172
+ }
10173
+ remove(postId) {
10174
+ this.sessions.delete(postId);
10175
+ }
10176
+ async kill(postId) {
10177
+ let session = this.sessions.get(postId);
10178
+ if (!session) {
10179
+ for (const [, s] of this.sessions) {
10180
+ if (s.taskId === postId) {
10181
+ session = s;
10182
+ break;
10183
+ }
10184
+ }
10185
+ }
10186
+ if (!session) return;
10187
+ if (session.answerResolver) {
10188
+ session.answerResolver("");
10189
+ session.answerResolver = null;
10190
+ }
10191
+ if (session.proc) {
10192
+ try {
10193
+ session.proc.kill("SIGTERM");
10194
+ } catch {
10195
+ }
10196
+ session.proc = null;
10197
+ }
10198
+ await killContainer(session.containerName);
10199
+ const removedPostId = session.postId;
10200
+ this.sessions.delete(removedPostId);
10201
+ this.emitter.emitSessionRemoved(removedPostId);
10202
+ }
10203
+ destroy() {
10204
+ this.resetBoardSession();
10205
+ for (const [, session] of this.sessions) {
10206
+ if (session.proc) {
10207
+ try {
10208
+ session.proc.kill("SIGTERM");
10209
+ } catch {
10210
+ }
10211
+ }
10212
+ void killContainer(session.containerName);
10213
+ }
10214
+ this.sessions.clear();
10215
+ }
10216
+ };
10217
+
10218
+ // ../../shared/all/helpers/terminal-ai/terminal-mode.ts
10219
+ var BARE_REPO_ROOT = "/home/claude/workspace";
10220
+ function posixNormalize(p) {
10221
+ const parts2 = p.split("/").filter(Boolean);
10222
+ const stack = [];
10223
+ for (const part of parts2) {
10224
+ if (part === ".") continue;
10225
+ if (part === "..") {
10226
+ if (stack.length) stack.pop();
10227
+ continue;
10228
+ }
10229
+ stack.push(part);
10230
+ }
10231
+ return "/" + stack.join("/");
10232
+ }
10233
+ function posixDirname(p) {
10234
+ const i = p.lastIndexOf("/");
10235
+ if (i <= 0) return "/";
10236
+ return p.slice(0, i);
10237
+ }
10238
+ function shEscape(s) {
10239
+ return `'${s.replace(/'/g, `'\\''`)}'`;
10240
+ }
10241
+ var TerminalModeEngine = class {
10242
+ constructor(core, emitter) {
10243
+ this.core = core;
10244
+ this.emitter = emitter;
10245
+ this.sessions = /* @__PURE__ */ new Map();
10246
+ }
10247
+ // ── Lifecycle / ModeEngine ──
10248
+ start(base, args) {
10249
+ const session = {
10250
+ ...base,
10251
+ openedFiles: [],
10252
+ buffers: {},
10253
+ exposed: []
10254
+ };
10255
+ this.sessions.set(session.postId, session);
9408
10256
  this.emitter.emitSessionUpdate(session);
9409
- this.emitter.addLog(session, "system", `${logPrefix} 2: Implement \u2500\u2500`);
9410
- const implResult = await this.runClaudeInContainer(
9411
- session,
9412
- implementPrompt,
9413
- tools,
9414
- session.claudeSessionId
9415
- );
9416
- session.claudeSessionId = implResult.sessionId || session.claudeSessionId;
9417
- if (!this.sessions.has(session.postId)) return;
9418
- if (enablePreview) {
9419
- session.phase = "preview";
9420
- session.step = 3;
10257
+ const onError = (err) => {
10258
+ if (!this.sessions.has(session.postId)) return;
10259
+ session.phase = "error";
10260
+ session.error = err instanceof Error ? err.message : String(err);
10261
+ this.emitter.addLog(session, "error", session.error);
9421
10262
  this.emitter.emitSessionUpdate(session);
9422
- this.emitter.addLog(session, "system", `${logPrefix} 3: Preview \u2500\u2500`);
9423
- const portsEnv = (await execInContainer(session.containerName, "echo -n $FACTIII_PORTS")).trim();
9424
- const publicPreviewEnabled = !!this.runnerToken;
9425
- if (publicPreviewEnabled && this.runnerToken) {
10263
+ };
10264
+ this.runBareSession(session, args.baseBranch, args.runtimes ?? []).catch(
10265
+ onError
10266
+ );
10267
+ }
10268
+ listSummaries() {
10269
+ const result = {};
10270
+ for (const [postId, session] of this.sessions) {
10271
+ result[postId] = toSummary(session);
10272
+ }
10273
+ return result;
10274
+ }
10275
+ get(postId) {
10276
+ return this.sessions.get(postId);
10277
+ }
10278
+ sessionWithLogs(postId) {
10279
+ const session = this.sessions.get(postId);
10280
+ if (!session) return null;
10281
+ return { ...toSummary(session), logs: session.logs };
10282
+ }
10283
+ has(postId) {
10284
+ return this.sessions.has(postId);
10285
+ }
10286
+ isRunning(postId) {
10287
+ const s = this.sessions.get(postId);
10288
+ return !!s && (s.phase === "starting" || s.phase === "bare-active");
10289
+ }
10290
+ remove(postId) {
10291
+ this.sessions.delete(postId);
10292
+ }
10293
+ async kill(postId) {
10294
+ let session = this.sessions.get(postId);
10295
+ if (!session) {
10296
+ for (const [, s] of this.sessions) {
10297
+ if (s.taskId === postId) {
10298
+ session = s;
10299
+ break;
10300
+ }
10301
+ }
10302
+ }
10303
+ if (!session) return;
10304
+ await killContainer(session.containerName);
10305
+ const removedPostId = session.postId;
10306
+ this.sessions.delete(removedPostId);
10307
+ this.emitter.emitSessionRemoved(removedPostId);
10308
+ }
10309
+ destroy() {
10310
+ for (const [, session] of this.sessions) {
10311
+ void killContainer(session.containerName);
10312
+ }
10313
+ this.sessions.clear();
10314
+ }
10315
+ /** Active container for a card, or null when none / terminated. */
10316
+ getCardContainerName(postId) {
10317
+ const session = this.sessions.get(String(postId));
10318
+ if (!session) return null;
10319
+ if (session.phase === "error" || session.phase === "done") return null;
10320
+ return session.containerName;
10321
+ }
10322
+ // ── container + Claude CLI, user drives ──
10323
+ async runBareSession(session, baseBranch, runtimes) {
10324
+ if (!this.core.isReady()) {
10325
+ throw new Error(
10326
+ 'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
10327
+ );
10328
+ }
10329
+ const config = this.core.readConfig();
10330
+ this.emitter.addLog(session, "system", "Starting container...");
10331
+ const onedrive = this.core.isOneDrive(config);
10332
+ await killContainer(session.containerName);
10333
+ await spawnContainer(session.containerName, void 0, { fuse: onedrive });
10334
+ if (onedrive) {
10335
+ const rotated = await writeRcloneMountConfig(
10336
+ session.containerName,
10337
+ this.core.getOneDriveConnection()
10338
+ );
10339
+ this.core.updateOneDriveRefreshToken(rotated);
10340
+ await mountWorkspace(session.containerName, this.core.remotePath());
10341
+ startOneDriveSyncWatcher(
10342
+ session.containerName,
10343
+ (pending, files) => this.emitter.emitOneDriveSync({
10344
+ postId: session.postId,
10345
+ pending,
10346
+ files
10347
+ })
10348
+ );
10349
+ this.emitter.addLog(session, "system", "OneDrive folder mounted.");
10350
+ } else {
10351
+ const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
10352
+ const branch = baseBranch || config.mainBranch || "main";
10353
+ const cacheKey = repoCacheKey(config.repoUrl);
10354
+ const { fromCache } = await cloneRepo(
10355
+ session.containerName,
10356
+ cloneUrl,
10357
+ branch,
10358
+ cacheKey
10359
+ );
10360
+ this.emitter.addLog(
10361
+ session,
10362
+ "system",
10363
+ fromCache ? "Repository updated from cache." : "Repository cloned."
10364
+ );
10365
+ await setGitIdentity(
10366
+ session.containerName,
10367
+ config.gitName,
10368
+ config.gitEmail
10369
+ );
10370
+ }
10371
+ if (this.core.runnerToken) {
10372
+ try {
9426
10373
  await installFrpcHarness({
9427
10374
  containerName: session.containerName,
9428
10375
  taskId: session.taskId,
9429
- runnerToken: this.runnerToken
10376
+ runnerToken: this.core.runnerToken
9430
10377
  });
9431
- }
9432
- const previewResult = await this.runClaudeInContainer(
9433
- session,
9434
- buildPrompt(prompts_default.preview, {
9435
- ports: portsEnv,
9436
- publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
9437
- }),
9438
- tools,
9439
- session.claudeSessionId
9440
- );
9441
- session.claudeSessionId = previewResult.sessionId || session.claudeSessionId;
9442
- if (!this.sessions.has(session.postId)) return;
9443
- const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
9444
- const previewPort = portMatch ? portMatch[1] : "3000";
9445
- const urlMatch = previewResult.text.match(/PREVIEW_URL:\s*(\S+)/);
9446
- if (publicPreviewEnabled && urlMatch) {
9447
- session.previewUrl = urlMatch[1];
10378
+ } catch (err) {
9448
10379
  this.emitter.addLog(
9449
10380
  session,
9450
10381
  "system",
9451
- `Public preview available at ${session.previewUrl}`
9452
- );
9453
- } else {
9454
- const previewUrl = await getContainerPort(
9455
- session.containerName,
9456
- previewPort
10382
+ `Port forwarding unavailable: ${err instanceof Error ? err.message : String(err)}`
9457
10383
  );
9458
- if (previewUrl) {
9459
- session.previewUrl = previewUrl;
9460
- this.emitter.addLog(
9461
- session,
9462
- "system",
9463
- `Preview available at ${session.previewUrl}`
9464
- );
9465
- } else {
9466
- this.emitter.addLog(
9467
- session,
9468
- "system",
9469
- "Could not map preview port. Server may not be running."
9470
- );
9471
- }
9472
10384
  }
9473
- this.emitter.emitSessionUpdate(session);
9474
- await new Promise((resolve) => {
9475
- session.previewResolver = resolve;
9476
- });
9477
- session.previewResolver = null;
9478
- if (!this.sessions.has(session.postId)) return;
9479
10385
  }
9480
- session.phase = "review";
9481
- session.step = 3;
10386
+ await installRuntimes(this.emitter, session, runtimes);
10387
+ session.phase = "bare-active";
9482
10388
  this.emitter.emitSessionUpdate(session);
9483
- this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
9484
- const reviewResult = await this.runClaudeInContainer(
10389
+ this.emitter.addLog(
9485
10390
  session,
9486
- buildPrompt(prompts_default.review),
9487
- tools,
9488
- session.claudeSessionId
10391
+ "system",
10392
+ "Container ready. Drive it from the terminal."
9489
10393
  );
9490
- session.claudeSessionId = reviewResult.sessionId || session.claudeSessionId;
9491
- if (!this.sessions.has(session.postId)) return;
9492
- session.reproSteps = reviewResult.text.trim();
9493
- await this.extractAndComplete(session);
9494
10394
  }
9495
- async extractAndComplete(session) {
10395
+ // ── file ops ──
10396
+ getBareContainer(postId) {
10397
+ const session = this.sessions.get(String(postId));
10398
+ if (!session) throw new Error("No active session for this card.");
10399
+ if (session.mode !== "bare") {
10400
+ throw new Error("File ops are only available in Bare mode.");
10401
+ }
10402
+ if (session.phase !== "bare-active" && session.phase !== "bare-review") {
10403
+ throw new Error("Container is not ready.");
10404
+ }
10405
+ return session.containerName;
10406
+ }
10407
+ /**
10408
+ * Resolve a repo-relative path to an absolute container path, rejecting
10409
+ * anything that escapes the repo root.
10410
+ */
10411
+ resolveBarePath(rel) {
10412
+ const normalized = posixNormalize("/" + (rel || "").replace(/^\/+/, ""));
10413
+ const abs = `${BARE_REPO_ROOT}${normalized === "/" ? "" : normalized}`;
10414
+ if (abs !== BARE_REPO_ROOT && !abs.startsWith(BARE_REPO_ROOT + "/")) {
10415
+ throw new Error("Path escapes repo root.");
10416
+ }
10417
+ return abs;
10418
+ }
10419
+ async bareFsList({
10420
+ postId,
10421
+ path: path7
10422
+ }) {
10423
+ const container = this.getBareContainer(postId);
10424
+ const abs = this.resolveBarePath(path7);
10425
+ const relRoot = abs.slice(BARE_REPO_ROOT.length) || "/";
10426
+ const out = await execInContainer(
10427
+ container,
10428
+ `cd ${shEscape(abs)} && for f in .* *; do
10429
+ case "$f" in
10430
+ "."|".."|".git"|"*") continue ;;
10431
+ esac
10432
+ [ -e "$f" ] || continue
10433
+ if [ -d "$f" ]; then echo "d/$f"; else echo "f/$f"; fi
10434
+ done | LC_ALL=C sort`
10435
+ );
10436
+ const entries = [];
10437
+ for (const line of out.split("\n")) {
10438
+ if (!line) continue;
10439
+ const sep = line.indexOf("/");
10440
+ if (sep < 0) continue;
10441
+ const ty = line.slice(0, sep);
10442
+ const name = line.slice(sep + 1);
10443
+ const type = ty === "d" ? "dir" : "file";
10444
+ entries.push({
10445
+ name,
10446
+ path: relRoot === "/" ? `/${name}` : `${relRoot}/${name}`,
10447
+ type
10448
+ });
10449
+ }
10450
+ entries.sort((a, b) => {
10451
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
10452
+ return a.name.localeCompare(b.name);
10453
+ });
10454
+ return entries;
10455
+ }
10456
+ async bareFsSearch({
10457
+ postId
10458
+ }) {
10459
+ const container = this.getBareContainer(postId);
10460
+ const root = BARE_REPO_ROOT;
10461
+ const out = await execInContainer(
10462
+ container,
10463
+ `cd ${shEscape(root)} && (git ls-files --cached --others --exclude-standard 2>/dev/null || find . -type f -not -path './.git/*' | sed 's|^\\./||') | head -n 5000`
10464
+ );
10465
+ const files = [];
10466
+ for (const line of out.split("\n")) {
10467
+ const p = line.trim();
10468
+ if (p) files.push("/" + p);
10469
+ }
10470
+ return files;
10471
+ }
10472
+ async bareFsRead({
10473
+ postId,
10474
+ path: path7
10475
+ }) {
10476
+ const container = this.getBareContainer(postId);
10477
+ const abs = this.resolveBarePath(path7);
10478
+ const b64 = (await execInContainer(container, `base64 -w 0 ${shEscape(abs)}`)).trim();
10479
+ return { content: b64, encoding: "base64" };
10480
+ }
10481
+ async bareFsWrite({
10482
+ postId,
10483
+ path: path7,
10484
+ content,
10485
+ encoding
10486
+ }) {
10487
+ const container = this.getBareContainer(postId);
10488
+ const abs = this.resolveBarePath(path7);
10489
+ const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
10490
+ if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
10491
+ throw new Error("Invalid base64 payload.");
10492
+ }
10493
+ await execInContainer(
10494
+ container,
10495
+ `mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
10496
+ );
10497
+ const session = this.sessions.get(String(postId));
10498
+ if (session) delete session.buffers[path7];
10499
+ }
10500
+ bareEditorState({
10501
+ postId: rawPostId
10502
+ }) {
10503
+ const postId = String(rawPostId);
10504
+ const session = this.sessions.get(postId);
10505
+ if (!session || session.mode !== "bare") {
10506
+ return { openedFiles: [], buffers: {} };
10507
+ }
10508
+ return {
10509
+ openedFiles: session.openedFiles.slice(),
10510
+ buffers: { ...session.buffers }
10511
+ };
10512
+ }
10513
+ bareOpenFile({
10514
+ postId: rawPostId,
10515
+ path: path7
10516
+ }) {
10517
+ const session = this.sessions.get(String(rawPostId));
10518
+ if (!session || session.mode !== "bare") return;
10519
+ if (!session.openedFiles.includes(path7)) {
10520
+ session.openedFiles.push(path7);
10521
+ }
10522
+ }
10523
+ bareCloseFile({
10524
+ postId: rawPostId,
10525
+ path: path7
10526
+ }) {
10527
+ const session = this.sessions.get(String(rawPostId));
10528
+ if (!session || session.mode !== "bare") return;
10529
+ session.openedFiles = session.openedFiles.filter((p) => p !== path7);
10530
+ delete session.buffers[path7];
10531
+ }
10532
+ bareBufferSet({
10533
+ postId: rawPostId,
10534
+ path: path7,
10535
+ content
10536
+ }) {
10537
+ const session = this.sessions.get(String(rawPostId));
10538
+ if (!session || session.mode !== "bare") return;
10539
+ session.buffers[path7] = content;
10540
+ }
10541
+ async bareFsDelete({
10542
+ postId,
10543
+ path: path7
10544
+ }) {
10545
+ const container = this.getBareContainer(postId);
10546
+ const abs = this.resolveBarePath(path7);
10547
+ if (abs === BARE_REPO_ROOT) {
10548
+ throw new Error("Refusing to delete the repo root.");
10549
+ }
10550
+ await execInContainer(container, `rm -rf ${shEscape(abs)}`);
10551
+ }
10552
+ async bareFsMkdir({
10553
+ postId,
10554
+ path: path7
10555
+ }) {
10556
+ const container = this.getBareContainer(postId);
10557
+ const abs = this.resolveBarePath(path7);
10558
+ await execInContainer(container, `mkdir -p ${shEscape(abs)}`);
10559
+ }
10560
+ async bareReview({
10561
+ postId: rawPostId
10562
+ }) {
10563
+ const postId = String(rawPostId);
10564
+ const session = this.sessions.get(postId);
10565
+ if (!session) throw new Error("No active session for this card.");
10566
+ if (session.mode !== "bare") {
10567
+ throw new Error("bareReview is only valid in Bare mode.");
10568
+ }
10569
+ if (session.phase !== "bare-active" && session.phase !== "bare-review") {
10570
+ throw new Error("Container is not ready.");
10571
+ }
10572
+ const config = this.core.readConfig();
10573
+ if (this.core.isOneDrive(config)) {
10574
+ session.diff = "";
10575
+ session.changedFiles = [];
10576
+ session.commitMessage = "";
10577
+ session.phase = "bare-review";
10578
+ this.emitter.emitSessionUpdate(session);
10579
+ return { diff: "", changedFiles: [], commitMessage: "" };
10580
+ }
9496
10581
  this.emitter.addLog(session, "system", "Extracting changes...");
9497
10582
  const { diff, changedFiles } = await extractDiff(session.containerName);
9498
10583
  session.diff = diff;
@@ -9504,68 +10589,424 @@ var BoardAgentEngine = class _BoardAgentEngine {
9504
10589
  `${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
9505
10590
  );
9506
10591
  this.emitter.addLog(session, "system", "Generating commit message...");
9507
- session.commitMessage = await generateCommitMessage(
9508
- session.containerName,
9509
- diff
9510
- );
10592
+ try {
10593
+ session.commitMessage = await generateCommitMessage(
10594
+ session.containerName,
10595
+ diff
10596
+ );
10597
+ } catch {
10598
+ session.commitMessage = "WIP: changes from bare session";
10599
+ }
9511
10600
  } else {
10601
+ session.commitMessage = "";
9512
10602
  this.emitter.addLog(session, "system", "No files changed.");
9513
10603
  }
9514
- session.phase = "complete";
10604
+ session.phase = "bare-review";
9515
10605
  this.emitter.emitSessionUpdate(session);
10606
+ return {
10607
+ diff: session.diff,
10608
+ changedFiles: session.changedFiles,
10609
+ commitMessage: session.commitMessage
10610
+ };
9516
10611
  }
9517
- cardFinishPreview({ postId: rawPostId }) {
10612
+ async bareGitStatus({
10613
+ postId: rawPostId
10614
+ }) {
9518
10615
  const postId = String(rawPostId);
9519
10616
  const session = this.sessions.get(postId);
9520
10617
  if (!session) throw new Error("No active session for this card.");
9521
- if (session.phase !== "preview")
9522
- throw new Error("Session is not in preview phase.");
9523
- if (!session.previewResolver) return;
9524
- this.emitter.addLog(session, "system", "Finishing preview...");
9525
- session.previewResolver();
10618
+ if (session.mode !== "bare") {
10619
+ throw new Error("bareGitStatus is only valid in Bare mode.");
10620
+ }
10621
+ if (session.phase !== "bare-active" && session.phase !== "bare-review") {
10622
+ throw new Error("Container is not ready.");
10623
+ }
10624
+ if (this.core.isOneDrive(this.core.readConfig())) {
10625
+ return { diff: "", changedFiles: [] };
10626
+ }
10627
+ return await extractDiff(session.containerName);
9526
10628
  }
9527
- cardSessions() {
9528
- const result = {};
9529
- for (const [postId, session] of this.sessions) {
9530
- result[postId] = toSummary(session);
10629
+ async bareListTerminals({
10630
+ postId: rawPostId
10631
+ }) {
10632
+ const postId = String(rawPostId);
10633
+ const session = this.sessions.get(postId);
10634
+ if (!session) return [];
10635
+ if (session.mode !== "bare") return [];
10636
+ if (session.phase !== "bare-active" && session.phase !== "bare-review") {
10637
+ return [];
10638
+ }
10639
+ const sanitizedPostId = postId.replace(/[^a-zA-Z0-9._-]/g, "_");
10640
+ const prefix = `terminal-${sanitizedPostId}-`;
10641
+ try {
10642
+ const stdout = await execInContainer(
10643
+ session.containerName,
10644
+ `tmux list-panes -a -F '#{session_name}|#{pane_current_command}' 2>/dev/null || true`
10645
+ );
10646
+ const byId = /* @__PURE__ */ new Map();
10647
+ for (const raw of stdout.split("\n")) {
10648
+ const line = raw.trim();
10649
+ if (!line.startsWith(prefix)) continue;
10650
+ const rest = line.slice(prefix.length);
10651
+ const sep = rest.indexOf("|");
10652
+ if (sep === -1) continue;
10653
+ const id = rest.slice(0, sep);
10654
+ const cmd = rest.slice(sep + 1).trim() || "bash";
10655
+ if (!byId.has(id)) byId.set(id, cmd);
10656
+ }
10657
+ return Array.from(byId, ([id, name]) => ({ id, name })).sort(
10658
+ (a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
10659
+ );
10660
+ } catch {
10661
+ return [];
9531
10662
  }
9532
- return result;
9533
10663
  }
9534
- cardSession({
9535
- postId
10664
+ // ── port forwarding (factiii-expose / factiii-unexpose) ──
10665
+ async bareExpose({
10666
+ postId,
10667
+ port,
10668
+ name
9536
10669
  }) {
10670
+ const container = this.getBareContainer(postId);
9537
10671
  const session = this.sessions.get(String(postId));
9538
- if (!session) return null;
9539
- return { ...toSummary(session), logs: session.logs };
10672
+ const safeName = name.trim();
10673
+ if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
10674
+ throw new Error("Name must be letters, numbers, or dashes.");
10675
+ }
10676
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
10677
+ throw new Error("Port must be between 1 and 65535.");
10678
+ }
10679
+ const out = (await execInContainer(
10680
+ container,
10681
+ `factiii-expose ${port} ${shEscape(safeName)} 2>&1`
10682
+ )).trim();
10683
+ const url2 = out.split("\n").filter(Boolean).pop() || "";
10684
+ if (!/^https?:\/\//.test(url2)) {
10685
+ throw new Error(out || "Failed to expose port.");
10686
+ }
10687
+ const entry = { name: safeName, port, url: url2 };
10688
+ session.exposed = session.exposed.filter((e) => e.name !== safeName);
10689
+ session.exposed.push(entry);
10690
+ return entry;
10691
+ }
10692
+ async bareUnexpose({
10693
+ postId,
10694
+ name
10695
+ }) {
10696
+ const container = this.getBareContainer(postId);
10697
+ const session = this.sessions.get(String(postId));
10698
+ const safeName = name.trim();
10699
+ if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
10700
+ throw new Error("Invalid tunnel name.");
10701
+ }
10702
+ await execInContainer(
10703
+ container,
10704
+ `factiii-unexpose ${shEscape(safeName)} 2>&1 || true`
10705
+ );
10706
+ session.exposed = session.exposed.filter((e) => e.name !== safeName);
9540
10707
  }
9541
- async cardDismiss({ postId }) {
10708
+ bareListExposed({
10709
+ postId
10710
+ }) {
9542
10711
  const session = this.sessions.get(String(postId));
9543
- if (!session) return;
9544
- await killContainer(session.containerName);
9545
- this.sessions.delete(String(postId));
9546
- this.emitter.emitSessionRemoved(String(postId));
10712
+ if (!session || session.mode !== "bare") return [];
10713
+ return session.exposed.slice();
10714
+ }
10715
+ };
10716
+
10717
+ // ../../shared/all/helpers/board-agent-core/engine.ts
10718
+ function sanitizeTaskId(taskId) {
10719
+ const sanitized = String(taskId).replace(/[^a-zA-Z0-9_.-]/g, "");
10720
+ if (!sanitized) throw new Error("Invalid taskId");
10721
+ return sanitized;
10722
+ }
10723
+ var BoardAgentEngine = class {
10724
+ constructor(options) {
10725
+ this.core = new ContainerCore(options);
10726
+ this.emitter = options.emitter;
10727
+ this.claude = new ClaudeModeEngine(this.core, this.emitter);
10728
+ this.terminal = new TerminalModeEngine(this.core, this.emitter);
10729
+ }
10730
+ /** The mode engine that currently owns this postId, if any. */
10731
+ owner(postId) {
10732
+ if (this.claude.has(postId)) return this.claude;
10733
+ if (this.terminal.has(postId)) return this.terminal;
10734
+ return null;
10735
+ }
10736
+ // ── Config / setup (→ core) ──
10737
+ getConfig() {
10738
+ return this.core.getConfig();
10739
+ }
10740
+ setConfig(partial) {
10741
+ return this.core.setConfig(partial);
10742
+ }
10743
+ clearToken(key) {
10744
+ return this.core.clearToken(key);
9547
10745
  }
9548
10746
  setupStatus() {
9549
- if (this.containerName) return { phase: "ready" };
9550
- if (this.setupPromise) return { phase: "running" };
9551
- return { phase: "idle" };
10747
+ return this.core.setupStatus();
9552
10748
  }
9553
- async ensureSetup() {
9554
- await this.ensureContainer();
9555
- return { ready: true };
10749
+ ensureSetup() {
10750
+ return this.core.ensureSetup();
9556
10751
  }
9557
- isReady() {
9558
- return this.containerName !== null;
10752
+ cardRemoteBranches() {
10753
+ return this.core.cardRemoteBranches();
9559
10754
  }
9560
- async cardRemoteBranches() {
9561
- const config = this.configProvider.readConfig(this.spaceSlug);
9562
- if (!config.githubToken || !config.repoUrl) return [];
9563
- if (!this.isReady()) {
9564
- throw new Error(
9565
- 'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
9566
- );
10755
+ // Start the host-run device-code flow and return the code to show the user.
10756
+ // The host polls Microsoft and pushes one `onedrive-auth` event when done.
10757
+ async onedriveStartDeviceCode(p) {
10758
+ const start = await startDeviceCode(p.clientId);
10759
+ console.log(
10760
+ `[onedrive] device-code started: userCode=${start.userCode} expiresIn=${start.expiresIn}s interval=${start.interval}s`
10761
+ );
10762
+ void this.pollDeviceCodeUntilDone(
10763
+ p.clientId,
10764
+ start.deviceCode,
10765
+ start.interval,
10766
+ start.expiresIn
10767
+ );
10768
+ return {
10769
+ userCode: start.userCode,
10770
+ verificationUri: start.verificationUri,
10771
+ expiresIn: start.expiresIn
10772
+ };
10773
+ }
10774
+ async pollDeviceCodeUntilDone(clientId, deviceCode, interval, expiresIn) {
10775
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
10776
+ const deadline = Date.now() + expiresIn * 1e3;
10777
+ let wait = Math.max(interval, 5);
10778
+ while (Date.now() < deadline) {
10779
+ await sleep(wait * 1e3);
10780
+ let res;
10781
+ try {
10782
+ res = await pollDeviceCode(clientId, deviceCode);
10783
+ } catch (err) {
10784
+ console.log(`[onedrive] poll threw (will retry): ${String(err)}`);
10785
+ continue;
10786
+ }
10787
+ console.log(`[onedrive] poll status=${res.status}`);
10788
+ if (res.status === "pending") continue;
10789
+ if (res.status === "slow_down") {
10790
+ wait += 5;
10791
+ continue;
10792
+ }
10793
+ if (res.status === "complete") {
10794
+ try {
10795
+ this.core.saveOneDriveConnection({
10796
+ refreshToken: res.refreshToken,
10797
+ clientId,
10798
+ ...res.appFolder
10799
+ });
10800
+ console.log("[onedrive] connection saved locally");
10801
+ } catch (err) {
10802
+ console.log(
10803
+ `[onedrive] saveOneDriveConnection failed: ${String(err)}`
10804
+ );
10805
+ this.emitter.emitOneDriveAuth({
10806
+ status: "error",
10807
+ message: "Failed to store the OneDrive connection on the host."
10808
+ });
10809
+ return;
10810
+ }
10811
+ this.emitter.emitOneDriveAuth({ status: "complete" });
10812
+ return;
10813
+ }
10814
+ this.emitter.emitOneDriveAuth({
10815
+ status: res.status,
10816
+ message: res.status === "error" ? res.message : void 0
10817
+ });
10818
+ return;
9567
10819
  }
9568
- return listRemoteBranches(this.containerName);
10820
+ console.log("[onedrive] device code reached deadline without completion");
10821
+ this.emitter.emitOneDriveAuth({ status: "expired" });
10822
+ }
10823
+ onedriveDisconnect() {
10824
+ this.core.clearOneDriveConnection();
10825
+ return { success: true };
10826
+ }
10827
+ // Unmount the workspace (flushing writes) and persist the rotated refresh
10828
+ // token. No-op for non-OneDrive spaces or when nothing is mounted.
10829
+ async onedriveTeardown(p) {
10830
+ const postId = String(p.postId);
10831
+ const session = this.owner(postId)?.get(postId);
10832
+ if (!session) return { success: true };
10833
+ stopOneDriveSyncWatcher(session.containerName);
10834
+ if (!this.core.isOneDrive(this.core.readConfig())) {
10835
+ return { success: true };
10836
+ }
10837
+ try {
10838
+ await unmountWorkspace(session.containerName);
10839
+ } catch {
10840
+ }
10841
+ const refreshToken = await readbackRefreshToken(
10842
+ session.containerName
10843
+ ).catch(() => null);
10844
+ if (refreshToken) this.core.updateOneDriveRefreshToken(refreshToken);
10845
+ return { success: true };
10846
+ }
10847
+ // Pending upload count to seed the UI (0 = synced); live updates come via the
10848
+ // onedrive-sync push. Synced for non-OneDrive spaces or idle sessions.
10849
+ onedriveStatus(p) {
10850
+ const postId = String(p.postId);
10851
+ const session = this.owner(postId)?.get(postId);
10852
+ if (!session || !this.core.isOneDrive(this.core.readConfig())) {
10853
+ return { syncing: false, pending: 0, files: [] };
10854
+ }
10855
+ const pending = onedrivePendingUploads(session.containerName);
10856
+ const files = onedriveUploadingFiles(session.containerName);
10857
+ return { syncing: pending > 0, pending, files };
10858
+ }
10859
+ // ── Ask AI + claude-only controls (→ claude) ──
10860
+ ask(params, sendLog) {
10861
+ return this.claude.ask(params, sendLog);
10862
+ }
10863
+ resetBoardSession() {
10864
+ this.claude.resetBoardSession();
10865
+ }
10866
+ boardSession() {
10867
+ return this.claude.boardSession();
10868
+ }
10869
+ cardStartPreview(payload) {
10870
+ return this.claude.cardStartPreview(payload);
10871
+ }
10872
+ cardAnswerQuestion(payload) {
10873
+ this.claude.cardAnswerQuestion(payload);
10874
+ }
10875
+ cardFeedback(payload) {
10876
+ this.claude.cardFeedback(payload);
10877
+ }
10878
+ // ── Terminal ops (→ terminal) ──
10879
+ bareFsList(p) {
10880
+ return this.terminal.bareFsList(p);
10881
+ }
10882
+ bareFsSearch(p) {
10883
+ return this.terminal.bareFsSearch(p);
10884
+ }
10885
+ bareFsRead(p) {
10886
+ return this.terminal.bareFsRead(p);
10887
+ }
10888
+ bareFsWrite(p) {
10889
+ return this.terminal.bareFsWrite(p);
10890
+ }
10891
+ bareFsDelete(p) {
10892
+ return this.terminal.bareFsDelete(p);
10893
+ }
10894
+ bareFsMkdir(p) {
10895
+ return this.terminal.bareFsMkdir(p);
10896
+ }
10897
+ bareReview(p) {
10898
+ return this.terminal.bareReview(p);
10899
+ }
10900
+ bareGitStatus(p) {
10901
+ return this.terminal.bareGitStatus(p);
10902
+ }
10903
+ bareListTerminals(p) {
10904
+ return this.terminal.bareListTerminals(p);
10905
+ }
10906
+ bareEditorState(p) {
10907
+ return this.terminal.bareEditorState(p);
10908
+ }
10909
+ bareOpenFile(p) {
10910
+ this.terminal.bareOpenFile(p);
10911
+ }
10912
+ bareCloseFile(p) {
10913
+ this.terminal.bareCloseFile(p);
10914
+ }
10915
+ bareBufferSet(p) {
10916
+ this.terminal.bareBufferSet(p);
10917
+ }
10918
+ bareExpose(p) {
10919
+ return this.terminal.bareExpose(p);
10920
+ }
10921
+ bareUnexpose(p) {
10922
+ return this.terminal.bareUnexpose(p);
10923
+ }
10924
+ bareListExposed(p) {
10925
+ return this.terminal.bareListExposed(p);
10926
+ }
10927
+ // ── Cross-mode coordinators ──
10928
+ async cardExecute(payload) {
10929
+ const {
10930
+ postId: rawPostId,
10931
+ prompt: prompt2,
10932
+ baseBranch,
10933
+ targetBranch,
10934
+ taskTitle,
10935
+ effort,
10936
+ mode,
10937
+ runtimes
10938
+ } = payload;
10939
+ const postId = String(rawPostId);
10940
+ if (this.claude.isRunning(postId) || this.terminal.isRunning(postId)) {
10941
+ throw new Error("A session is already running for this card.");
10942
+ }
10943
+ const stale = this.owner(postId);
10944
+ if (stale) {
10945
+ const s = stale.get(postId);
10946
+ await killContainer(s.containerName);
10947
+ stale.remove(postId);
10948
+ }
10949
+ const taskId = sanitizeTaskId(`${postId}-${Date.now()}`);
10950
+ const base = {
10951
+ taskId,
10952
+ postId,
10953
+ phase: "starting",
10954
+ mode,
10955
+ step: 1,
10956
+ logs: [],
10957
+ diff: "",
10958
+ changedFiles: [],
10959
+ commitMessage: "",
10960
+ previewUrl: "",
10961
+ previewRunning: false,
10962
+ reproSteps: "",
10963
+ pendingQuestion: "",
10964
+ error: "",
10965
+ startedAt: Date.now(),
10966
+ taskTitle: taskTitle || "",
10967
+ targetBranch: targetBranch || "",
10968
+ effort: effort || "high",
10969
+ todos: [],
10970
+ containerName: `factiii-${this.core.instancePrefix}-card-${taskId}`
10971
+ };
10972
+ if (mode === "bare") {
10973
+ this.terminal.start(base, {
10974
+ baseBranch: baseBranch ?? "",
10975
+ runtimes: runtimes ?? []
10976
+ });
10977
+ } else {
10978
+ this.claude.start(base, {
10979
+ prompt: prompt2,
10980
+ baseBranch: baseBranch ?? "",
10981
+ runtimes: runtimes ?? []
10982
+ });
10983
+ }
10984
+ return { taskId, postId };
10985
+ }
10986
+ /**
10987
+ * Look up the active container for a card. Used by the terminal bridge to
10988
+ * map postId to the docker container it should attach a shell to.
10989
+ */
10990
+ getCardContainerName(postId) {
10991
+ const session = this.claude.get(String(postId)) ?? this.terminal.get(String(postId));
10992
+ if (!session) return null;
10993
+ if (session.phase === "error" || session.phase === "done") return null;
10994
+ return session.containerName;
10995
+ }
10996
+ cardSessions() {
10997
+ return { ...this.terminal.listSummaries(), ...this.claude.listSummaries() };
10998
+ }
10999
+ cardSession({ postId }) {
11000
+ return this.claude.sessionWithLogs(String(postId)) ?? this.terminal.sessionWithLogs(String(postId));
11001
+ }
11002
+ async cardDismiss({ postId }) {
11003
+ const owner = this.owner(String(postId));
11004
+ if (!owner) return;
11005
+ const session = owner.get(String(postId));
11006
+ await this.onedriveTeardown({ postId: String(postId) });
11007
+ await killContainer(session.containerName);
11008
+ owner.remove(String(postId));
11009
+ this.emitter.emitSessionRemoved(String(postId));
9569
11010
  }
9570
11011
  async cardFinalize({
9571
11012
  postId: rawPostId,
@@ -9574,128 +11015,79 @@ var BoardAgentEngine = class _BoardAgentEngine {
9574
11015
  commitMessage
9575
11016
  }) {
9576
11017
  const postId = String(rawPostId);
9577
- const session = this.sessions.get(postId);
9578
- if (!session) throw new Error("No active session for this card.");
11018
+ const owner = this.owner(postId);
11019
+ if (!owner) throw new Error("No active session for this card.");
11020
+ const session = owner.get(postId);
11021
+ const config = this.core.readConfig();
11022
+ if (this.core.isOneDrive(config)) {
11023
+ if (apply) {
11024
+ session.phase = "done";
11025
+ this.emitter.addLog(session, "system", "Changes are live on OneDrive.");
11026
+ this.emitter.emitSessionUpdate(session);
11027
+ return { applied: true, branchName: "" };
11028
+ }
11029
+ await this.onedriveTeardown({ postId });
11030
+ await killContainer(session.containerName);
11031
+ owner.remove(postId);
11032
+ this.emitter.emitSessionRemoved(postId);
11033
+ return { applied: false, branchName: "" };
11034
+ }
9579
11035
  if (apply && branchName) {
9580
- const config = this.configProvider.readConfig(this.spaceSlug);
9581
- await commitAndPush(
9582
- session.containerName,
9583
- branchName,
9584
- commitMessage || `Board AI: ${branchName}`,
9585
- {
9586
- gitName: config.gitName,
9587
- gitEmail: config.gitEmail
11036
+ let pushResult;
11037
+ try {
11038
+ pushResult = await commitAndPush(
11039
+ session.containerName,
11040
+ branchName,
11041
+ commitMessage || `Board AI: ${branchName}`,
11042
+ {
11043
+ gitName: config.gitName,
11044
+ gitEmail: config.gitEmail
11045
+ }
11046
+ );
11047
+ } catch (err) {
11048
+ if (err instanceof NoChangesError) {
11049
+ this.emitter.addLog(
11050
+ session,
11051
+ "system",
11052
+ "No changes to push - container kept alive for inspection."
11053
+ );
11054
+ } else {
11055
+ this.emitter.addLog(
11056
+ session,
11057
+ "error",
11058
+ `Push failed: ${err instanceof Error ? err.message : String(err)}`
11059
+ );
9588
11060
  }
9589
- );
9590
- }
9591
- await killContainer(session.containerName);
9592
- if (apply) {
11061
+ this.emitter.emitSessionUpdate(session);
11062
+ throw err;
11063
+ }
9593
11064
  session.phase = "done";
9594
11065
  this.emitter.addLog(
9595
11066
  session,
9596
11067
  "system",
9597
- `Changes committed to ${branchName}`
11068
+ `Pushed ${pushResult.pushedSha.slice(0, 8)} to origin/${pushResult.branch}`
9598
11069
  );
9599
11070
  this.emitter.emitSessionUpdate(session);
9600
- setTimeout(() => {
9601
- if (this.sessions.get(postId)?.phase === "done") {
9602
- this.sessions.delete(postId);
9603
- this.emitter.emitSessionRemoved(postId);
9604
- }
9605
- }, 1e4);
9606
- } else {
9607
- this.sessions.delete(postId);
9608
- this.emitter.emitSessionRemoved(postId);
11071
+ return { applied: true, branchName: pushResult.branch };
9609
11072
  }
11073
+ await killContainer(session.containerName);
11074
+ owner.remove(postId);
11075
+ this.emitter.emitSessionRemoved(postId);
9610
11076
  return { applied: apply, branchName: branchName || "" };
9611
11077
  }
9612
- cardAnswerQuestion({
9613
- postId: rawPostId,
9614
- answer
9615
- }) {
9616
- const postId = String(rawPostId);
9617
- const session = this.sessions.get(postId);
9618
- if (!session || session.phase !== "awaiting-input" || !session.answerResolver)
9619
- return;
9620
- session.answerResolver(answer);
9621
- }
9622
- cardFeedback({
9623
- postId: rawPostId,
9624
- feedback
9625
- }) {
9626
- const postId = String(rawPostId);
9627
- const session = this.sessions.get(postId);
9628
- if (!session || session.phase !== "complete") return;
9629
- if (!session.claudeSessionId) return;
9630
- session.diff = "";
9631
- session.changedFiles = [];
9632
- session.commitMessage = "";
9633
- session.reproSteps = "";
9634
- const prompt2 = `The user reviewed your changes and has the following feedback:
9635
-
9636
- ${feedback}
9637
-
9638
- ${buildPrompt(prompts_default.implement)}`;
9639
- this.runImplementation(session, prompt2, "\u2500\u2500 Follow-up").catch((err) => {
9640
- if (!this.sessions.has(postId)) return;
9641
- session.phase = "error";
9642
- session.error = err instanceof Error ? err.message : String(err);
9643
- this.emitter.addLog(session, "error", session.error);
9644
- this.emitter.emitSessionUpdate(session);
9645
- });
9646
- }
9647
11078
  async cardKill({ postId: rawPostId }) {
9648
11079
  const postId = String(rawPostId);
9649
- let session = this.sessions.get(postId);
9650
- if (!session) {
9651
- for (const [, s] of this.sessions) {
9652
- if (s.taskId === rawPostId) {
9653
- session = s;
9654
- break;
9655
- }
9656
- }
9657
- }
9658
- if (!session) return;
9659
- if (session.answerResolver) {
9660
- session.answerResolver("");
9661
- session.answerResolver = null;
9662
- }
9663
- if (session.previewResolver) {
9664
- session.previewResolver();
9665
- session.previewResolver = null;
9666
- }
9667
- const proc = this.cardProcesses.get(session.postId);
9668
- if (proc) {
9669
- try {
9670
- proc.kill("SIGTERM");
9671
- } catch {
9672
- }
9673
- this.cardProcesses.delete(session.postId);
9674
- }
9675
- await killContainer(session.containerName);
9676
- const removedPostId = session.postId;
9677
- this.sessions.delete(removedPostId);
9678
- this.emitter.emitSessionRemoved(removedPostId);
11080
+ await this.onedriveTeardown({ postId });
11081
+ if (this.claude.has(postId)) return this.claude.kill(postId);
11082
+ if (this.terminal.has(postId)) return this.terminal.kill(postId);
11083
+ await this.claude.kill(postId);
11084
+ await this.terminal.kill(postId);
9679
11085
  }
9680
11086
  // ── Lifecycle ──
9681
11087
  destroy() {
9682
- this.resetBoardSession();
9683
- for (const [, proc] of this.cardProcesses) {
9684
- try {
9685
- proc.kill("SIGTERM");
9686
- } catch {
9687
- }
9688
- }
9689
- this.cardProcesses.clear();
9690
- for (const [, session] of this.sessions) {
9691
- void killContainer(session.containerName);
9692
- }
9693
- this.sessions.clear();
9694
- if (this.containerName) {
9695
- void killContainer(this.containerName);
9696
- this.containerName = null;
9697
- this.setupPromise = null;
9698
- }
11088
+ this.claude.destroy();
11089
+ this.terminal.destroy();
11090
+ this.core.destroy();
9699
11091
  }
9700
11092
  /**
9701
11093
  * Route a single action/payload pair to the right engine method.
@@ -9725,13 +11117,16 @@ ${buildPrompt(prompts_default.implement)}`;
9725
11117
  case "resetBoardSession":
9726
11118
  this.resetBoardSession();
9727
11119
  return void 0;
11120
+ case "boardSession":
11121
+ return this.boardSession();
9728
11122
  case "cardExecute":
9729
11123
  return await this.cardExecute(
9730
11124
  payload
9731
11125
  );
9732
- case "cardFinishPreview":
9733
- this.cardFinishPreview(payload);
9734
- return void 0;
11126
+ case "cardStartPreview":
11127
+ return await this.cardStartPreview(
11128
+ payload
11129
+ );
9735
11130
  case "cardSessions":
9736
11131
  return this.cardSessions();
9737
11132
  case "cardSession":
@@ -9747,6 +11142,20 @@ ${buildPrompt(prompts_default.implement)}`;
9747
11142
  return await this.ensureSetup();
9748
11143
  case "cardRemoteBranches":
9749
11144
  return await this.cardRemoteBranches();
11145
+ case "onedriveStartDeviceCode":
11146
+ return await this.onedriveStartDeviceCode(
11147
+ payload
11148
+ );
11149
+ case "onedriveDisconnect":
11150
+ return this.onedriveDisconnect();
11151
+ case "onedriveTeardown":
11152
+ return await this.onedriveTeardown(
11153
+ payload
11154
+ );
11155
+ case "onedriveStatus":
11156
+ return this.onedriveStatus(
11157
+ payload
11158
+ );
9750
11159
  case "cardFinalize":
9751
11160
  return await this.cardFinalize(
9752
11161
  payload
@@ -9760,6 +11169,77 @@ ${buildPrompt(prompts_default.implement)}`;
9760
11169
  case "cardKill":
9761
11170
  await this.cardKill(payload);
9762
11171
  return void 0;
11172
+ case "bareFsList":
11173
+ return await this.bareFsList(
11174
+ payload
11175
+ );
11176
+ case "bareFsSearch":
11177
+ return await this.bareFsSearch(
11178
+ payload
11179
+ );
11180
+ case "bareFsRead":
11181
+ return await this.bareFsRead(
11182
+ payload
11183
+ );
11184
+ case "bareFsWrite":
11185
+ await this.bareFsWrite(
11186
+ payload
11187
+ );
11188
+ return void 0;
11189
+ case "bareFsDelete":
11190
+ await this.bareFsDelete(
11191
+ payload
11192
+ );
11193
+ return void 0;
11194
+ case "bareFsMkdir":
11195
+ await this.bareFsMkdir(
11196
+ payload
11197
+ );
11198
+ return void 0;
11199
+ case "bareReview":
11200
+ return await this.bareReview(
11201
+ payload
11202
+ );
11203
+ case "bareGitStatus":
11204
+ return await this.bareGitStatus(
11205
+ payload
11206
+ );
11207
+ case "bareListTerminals":
11208
+ return await this.bareListTerminals(
11209
+ payload
11210
+ );
11211
+ case "bareEditorState":
11212
+ return this.bareEditorState(
11213
+ payload
11214
+ );
11215
+ case "bareOpenFile":
11216
+ this.bareOpenFile(
11217
+ payload
11218
+ );
11219
+ return void 0;
11220
+ case "bareCloseFile":
11221
+ this.bareCloseFile(
11222
+ payload
11223
+ );
11224
+ return void 0;
11225
+ case "bareBufferSet":
11226
+ this.bareBufferSet(
11227
+ payload
11228
+ );
11229
+ return void 0;
11230
+ case "bareExpose":
11231
+ return await this.bareExpose(
11232
+ payload
11233
+ );
11234
+ case "bareUnexpose":
11235
+ await this.bareUnexpose(
11236
+ payload
11237
+ );
11238
+ return void 0;
11239
+ case "bareListExposed":
11240
+ return this.bareListExposed(
11241
+ payload
11242
+ );
9763
11243
  default:
9764
11244
  throw new Error(`Unknown board-agent action: ${String(action)}`);
9765
11245
  }
@@ -9816,7 +11296,7 @@ function makeChunkReassembler() {
9816
11296
 
9817
11297
  // src/daemon.ts
9818
11298
  var import_node_datachannel = require("node-datachannel");
9819
- var import_path4 = __toESM(require("path"));
11299
+ var import_path5 = __toESM(require("path"));
9820
11300
 
9821
11301
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
9822
11302
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -10530,8 +12010,8 @@ var BaseXHR = class extends Polling {
10530
12010
  /**
10531
12011
  * Sends data.
10532
12012
  *
10533
- * @param {String} data to send.
10534
- * @param {Function} called upon flush.
12013
+ * @param {String} data - data to send.
12014
+ * @param {Function} fn - called upon flush.
10535
12015
  * @private
10536
12016
  */
10537
12017
  doWrite(data, fn) {
@@ -10751,8 +12231,11 @@ var XHR = class extends BaseXHR {
10751
12231
 
10752
12232
  // ../../node_modules/engine.io-client/node_modules/ws/wrapper.mjs
10753
12233
  var import_stream = __toESM(require_stream(), 1);
12234
+ var import_extension = __toESM(require_extension(), 1);
12235
+ var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
10754
12236
  var import_receiver = __toESM(require_receiver(), 1);
10755
12237
  var import_sender = __toESM(require_sender(), 1);
12238
+ var import_subprotocol = __toESM(require_subprotocol(), 1);
10756
12239
  var import_websocket = __toESM(require_websocket(), 1);
10757
12240
  var import_websocket_server = __toESM(require_websocket_server(), 1);
10758
12241
 
@@ -10993,12 +12476,12 @@ function parse2(str) {
10993
12476
  uri.queryKey = queryKey(uri, uri["query"]);
10994
12477
  return uri;
10995
12478
  }
10996
- function pathNames(obj, path6) {
10997
- const regx = /\/{2,9}/g, names = path6.replace(regx, "/").split("/");
10998
- if (path6.slice(0, 1) == "/" || path6.length === 0) {
12479
+ function pathNames(obj, path7) {
12480
+ const regx = /\/{2,9}/g, names = path7.replace(regx, "/").split("/");
12481
+ if (path7.slice(0, 1) == "/" || path7.length === 0) {
10999
12482
  names.splice(0, 1);
11000
12483
  }
11001
- if (path6.slice(-1) == "/") {
12484
+ if (path7.slice(-1) == "/") {
11002
12485
  names.splice(names.length - 1, 1);
11003
12486
  }
11004
12487
  return names;
@@ -11353,7 +12836,7 @@ var SocketWithoutUpgrade = class _SocketWithoutUpgrade extends import_component_
11353
12836
  /**
11354
12837
  * Sends a packet.
11355
12838
  *
11356
- * @param {String} type: packet type.
12839
+ * @param {String} type - packet type.
11357
12840
  * @param {String} data.
11358
12841
  * @param {Object} options.
11359
12842
  * @param {Function} fn - callback function.
@@ -11616,7 +13099,7 @@ var protocol2 = Socket.protocol;
11616
13099
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
11617
13100
  var import_debug7 = __toESM(require_src(), 1);
11618
13101
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
11619
- function url(uri, path6 = "", loc) {
13102
+ function url(uri, path7 = "", loc) {
11620
13103
  let obj = uri;
11621
13104
  loc = loc || typeof location !== "undefined" && location;
11622
13105
  if (null == uri)
@@ -11650,7 +13133,7 @@ function url(uri, path6 = "", loc) {
11650
13133
  obj.path = obj.path || "/";
11651
13134
  const ipv6 = obj.host.indexOf(":") !== -1;
11652
13135
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
11653
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path6;
13136
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path7;
11654
13137
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
11655
13138
  return obj;
11656
13139
  }
@@ -13284,8 +14767,8 @@ function lookup(uri, opts) {
13284
14767
  const parsed = url(uri, opts.path || "/socket.io");
13285
14768
  const source = parsed.source;
13286
14769
  const id = parsed.id;
13287
- const path6 = parsed.path;
13288
- const sameNamespace = cache[id] && path6 in cache[id]["nsps"];
14770
+ const path7 = parsed.path;
14771
+ const sameNamespace = cache[id] && path7 in cache[id]["nsps"];
13289
14772
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
13290
14773
  let io;
13291
14774
  if (newConnection) {
@@ -13310,44 +14793,296 @@ Object.assign(lookup, {
13310
14793
  connect: lookup
13311
14794
  });
13312
14795
 
13313
- // src/agent-adapter.ts
14796
+ // ../../shared/all/helpers/terminal-ai/terminal.ts
14797
+ var import_child_process6 = require("child_process");
14798
+ var import_dockerode = __toESM(require("dockerode"));
13314
14799
  var import_fs3 = __toESM(require("fs"));
13315
14800
  var import_os2 = __toESM(require("os"));
13316
14801
  var import_path3 = __toESM(require("path"));
13317
- var CONFIG_DIR2 = import_path3.default.join(import_os2.default.homedir(), ".factiii-runner");
14802
+ function resolveDocker() {
14803
+ if (process.env.DOCKER_HOST) return new import_dockerode.default();
14804
+ try {
14805
+ const out = (0, import_child_process6.execFileSync)(
14806
+ "docker",
14807
+ ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
14808
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
14809
+ ).trim();
14810
+ if (out.startsWith("unix://")) {
14811
+ const p = out.slice("unix://".length);
14812
+ if (import_fs3.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
14813
+ } else if (out) {
14814
+ process.env.DOCKER_HOST = out;
14815
+ return new import_dockerode.default();
14816
+ }
14817
+ } catch {
14818
+ }
14819
+ const candidates = [
14820
+ "/var/run/docker.sock",
14821
+ import_path3.default.join(import_os2.default.homedir(), ".docker/run/docker.sock"),
14822
+ import_path3.default.join(import_os2.default.homedir(), ".docker/desktop/docker.sock"),
14823
+ import_path3.default.join(import_os2.default.homedir(), ".orbstack/run/docker.sock"),
14824
+ import_path3.default.join(import_os2.default.homedir(), ".colima/default/docker.sock"),
14825
+ import_path3.default.join(import_os2.default.homedir(), ".rd/docker.sock")
14826
+ ];
14827
+ for (const p of candidates) {
14828
+ try {
14829
+ import_fs3.default.statSync(p);
14830
+ return new import_dockerode.default({ socketPath: p });
14831
+ } catch {
14832
+ }
14833
+ }
14834
+ return new import_dockerode.default();
14835
+ }
14836
+ var docker = resolveDocker();
14837
+ var TERMINAL_INIT_SCRIPT = [
14838
+ `export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`,
14839
+ `exec bash --norc -i`
14840
+ ].join("; ");
14841
+ function tmuxName(terminalKey) {
14842
+ const safe = terminalKey.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
14843
+ return `terminal-${safe}`;
14844
+ }
14845
+ var TerminalManager = class {
14846
+ constructor(send, getCardContainerName) {
14847
+ this.send = send;
14848
+ this.getCardContainerName = getCardContainerName;
14849
+ this.sessions = /* @__PURE__ */ new Map();
14850
+ }
14851
+ handle(msg) {
14852
+ switch (msg.op) {
14853
+ case "open":
14854
+ void this.open(msg);
14855
+ return;
14856
+ case "stdin":
14857
+ this.stdin(msg.sessionId, msg.data);
14858
+ return;
14859
+ case "resize":
14860
+ void this.resize(msg.sessionId, msg.cols, msg.rows);
14861
+ return;
14862
+ case "close":
14863
+ this.close(msg.sessionId);
14864
+ return;
14865
+ case "kill":
14866
+ void this.killTmuxSession(msg.postId, msg.terminalKey);
14867
+ return;
14868
+ }
14869
+ }
14870
+ async open(msg) {
14871
+ if (this.sessions.has(msg.sessionId)) {
14872
+ this.emit({
14873
+ type: "terminal",
14874
+ op: "error",
14875
+ sessionId: msg.sessionId,
14876
+ message: "Session already open."
14877
+ });
14878
+ return;
14879
+ }
14880
+ const containerName = this.getCardContainerName(msg.postId);
14881
+ if (!containerName) {
14882
+ this.emit({
14883
+ type: "terminal",
14884
+ op: "error",
14885
+ sessionId: msg.sessionId,
14886
+ message: `No active card session for postId=${msg.postId}.`
14887
+ });
14888
+ return;
14889
+ }
14890
+ const tmuxSession = tmuxName(msg.terminalKey);
14891
+ try {
14892
+ const exec = await docker.getContainer(containerName).exec({
14893
+ Cmd: [
14894
+ "tmux",
14895
+ // -u forces UTF-8 regardless of the locale. Slim base images often
14896
+ // ship without a UTF-8 locale, so tmux's auto-detect falls through to
14897
+ // ASCII line-draw chars (the "qqqq" mess in place of ─).
14898
+ "-u",
14899
+ "new-session",
14900
+ "-A",
14901
+ "-s",
14902
+ tmuxSession,
14903
+ "--",
14904
+ "bash",
14905
+ "-c",
14906
+ TERMINAL_INIT_SCRIPT
14907
+ ],
14908
+ AttachStdin: true,
14909
+ AttachStdout: true,
14910
+ AttachStderr: true,
14911
+ Tty: true,
14912
+ // Drop the user into the cloned repo so claude/git/etc. just work.
14913
+ WorkingDir: "/home/claude/workspace",
14914
+ Env: ["TERM=xterm-256color"],
14915
+ // The client follows up with a resize op carrying real dimensions.
14916
+ ConsoleSize: [msg.rows, msg.cols]
14917
+ });
14918
+ const stream = await exec.start({
14919
+ hijack: true,
14920
+ stdin: true,
14921
+ Tty: true
14922
+ });
14923
+ this.sessions.set(msg.sessionId, { exec, stream, containerName });
14924
+ stream.on("data", (chunk) => {
14925
+ this.emit({
14926
+ type: "terminal",
14927
+ op: "stdout",
14928
+ sessionId: msg.sessionId,
14929
+ data: chunk.toString("base64")
14930
+ });
14931
+ });
14932
+ stream.on("error", (err) => {
14933
+ this.cleanup(msg.sessionId);
14934
+ this.emit({
14935
+ type: "terminal",
14936
+ op: "error",
14937
+ sessionId: msg.sessionId,
14938
+ message: err.message
14939
+ });
14940
+ });
14941
+ stream.on("end", () => {
14942
+ void this.finalize(msg.sessionId, exec);
14943
+ });
14944
+ stream.on("close", () => {
14945
+ void this.finalize(msg.sessionId, exec);
14946
+ });
14947
+ this.emit({
14948
+ type: "terminal",
14949
+ op: "opened",
14950
+ sessionId: msg.sessionId,
14951
+ containerName
14952
+ });
14953
+ } catch (err) {
14954
+ this.emit({
14955
+ type: "terminal",
14956
+ op: "error",
14957
+ sessionId: msg.sessionId,
14958
+ message: err instanceof Error ? err.message : String(err)
14959
+ });
14960
+ }
14961
+ }
14962
+ stdin(sessionId, b64) {
14963
+ const session = this.sessions.get(sessionId);
14964
+ if (!session) return;
14965
+ try {
14966
+ session.stream.write(Buffer.from(b64, "base64"));
14967
+ } catch {
14968
+ }
14969
+ }
14970
+ async resize(sessionId, cols, rows) {
14971
+ const session = this.sessions.get(sessionId);
14972
+ if (!session) return;
14973
+ try {
14974
+ await session.exec.resize({ h: rows, w: cols });
14975
+ } catch {
14976
+ }
14977
+ }
14978
+ close(sessionId) {
14979
+ const session = this.sessions.get(sessionId);
14980
+ if (!session) return;
14981
+ try {
14982
+ session.stream.end();
14983
+ } catch {
14984
+ }
14985
+ }
14986
+ /** Tear down the tmux session for a tab. Used when the user explicitly
14987
+ * removes a tab (the X button), not on transient detach. */
14988
+ async killTmuxSession(postId, terminalKey) {
14989
+ const containerName = this.getCardContainerName(postId);
14990
+ if (!containerName) return;
14991
+ const session = tmuxName(terminalKey);
14992
+ try {
14993
+ const exec = await docker.getContainer(containerName).exec({
14994
+ Cmd: ["tmux", "kill-session", "-t", session],
14995
+ AttachStdout: false,
14996
+ AttachStderr: false,
14997
+ Tty: false
14998
+ });
14999
+ await exec.start({});
15000
+ } catch {
15001
+ }
15002
+ }
15003
+ destroyAll() {
15004
+ for (const sessionId of [...this.sessions.keys()]) {
15005
+ this.close(sessionId);
15006
+ }
15007
+ }
15008
+ // Called only once even if both 'end' and 'close' fire.
15009
+ async finalize(sessionId, exec) {
15010
+ if (!this.sessions.has(sessionId)) return;
15011
+ this.cleanup(sessionId);
15012
+ let code = null;
15013
+ try {
15014
+ const info = await exec.inspect();
15015
+ code = typeof info.ExitCode === "number" ? info.ExitCode : null;
15016
+ } catch {
15017
+ }
15018
+ this.emit({ type: "terminal", op: "exit", sessionId, code });
15019
+ }
15020
+ cleanup(sessionId) {
15021
+ this.sessions.delete(sessionId);
15022
+ }
15023
+ emit(msg) {
15024
+ const wire = JSON.stringify(msg);
15025
+ for (const frame of chunkWire(wire)) this.send(frame);
15026
+ }
15027
+ };
15028
+
15029
+ // src/agent-adapter.ts
15030
+ var import_fs4 = __toESM(require("fs"));
15031
+ var import_os3 = __toESM(require("os"));
15032
+ var import_path4 = __toESM(require("path"));
15033
+ var CONFIG_DIR2 = import_path4.default.join(import_os3.default.homedir(), ".factiii-runner");
15034
+ function safeSlug(spaceSlug) {
15035
+ return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
15036
+ }
13318
15037
  function configPath(spaceSlug) {
13319
- const safe = spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
13320
- return import_path3.default.join(CONFIG_DIR2, `space-${safe}.json`);
15038
+ return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.json`);
15039
+ }
15040
+ function connectionPath(spaceSlug) {
15041
+ return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
15042
+ }
15043
+ function writeJson(filePath, value2) {
15044
+ import_fs4.default.mkdirSync(CONFIG_DIR2, { recursive: true });
15045
+ import_fs4.default.writeFileSync(filePath, JSON.stringify(value2, null, 2));
15046
+ import_fs4.default.chmodSync(filePath, 384);
13321
15047
  }
13322
15048
  var localConfigProvider = {
13323
15049
  readConfig(spaceSlug) {
15050
+ let config = {};
13324
15051
  try {
13325
- const raw = import_fs3.default.readFileSync(configPath(spaceSlug), "utf-8");
13326
- const config = JSON.parse(raw);
13327
- return {
13328
- repoUrl: config.repoUrl || "",
13329
- mainBranch: config.mainBranch || "main",
13330
- gitName: config.gitName || "",
13331
- gitEmail: config.gitEmail || "",
13332
- claudeToken: config.claudeToken || "",
13333
- githubToken: config.githubToken || ""
13334
- };
15052
+ config = JSON.parse(
15053
+ import_fs4.default.readFileSync(configPath(spaceSlug), "utf-8")
15054
+ );
13335
15055
  } catch {
13336
- return {
13337
- repoUrl: "",
13338
- mainBranch: "main",
13339
- gitName: "",
13340
- gitEmail: "",
13341
- claudeToken: "",
13342
- githubToken: ""
13343
- };
13344
15056
  }
15057
+ return {
15058
+ repoUrl: config.repoUrl || "",
15059
+ mainBranch: config.mainBranch || "main",
15060
+ gitName: config.gitName || "",
15061
+ gitEmail: config.gitEmail || "",
15062
+ claudeToken: config.claudeToken || "",
15063
+ githubToken: config.githubToken || "",
15064
+ storageBackend: config.storageBackend || "github"
15065
+ };
13345
15066
  },
13346
15067
  writeConfig(spaceSlug, config) {
13347
- import_fs3.default.mkdirSync(CONFIG_DIR2, { recursive: true });
13348
- const filePath = configPath(spaceSlug);
13349
- import_fs3.default.writeFileSync(filePath, JSON.stringify(config, null, 2));
13350
- import_fs3.default.chmodSync(filePath, 384);
15068
+ writeJson(configPath(spaceSlug), config);
15069
+ },
15070
+ readOneDriveConnection(spaceSlug) {
15071
+ try {
15072
+ return JSON.parse(
15073
+ import_fs4.default.readFileSync(connectionPath(spaceSlug), "utf-8")
15074
+ );
15075
+ } catch {
15076
+ return null;
15077
+ }
15078
+ },
15079
+ writeOneDriveConnection(spaceSlug, connection) {
15080
+ const filePath = connectionPath(spaceSlug);
15081
+ if (!connection) {
15082
+ import_fs4.default.rmSync(filePath, { force: true });
15083
+ return;
15084
+ }
15085
+ writeJson(filePath, connection);
13351
15086
  }
13352
15087
  };
13353
15088
 
@@ -13362,6 +15097,14 @@ async function startDaemon(config) {
13362
15097
  });
13363
15098
  const engines = /* @__PURE__ */ new Map();
13364
15099
  const openChannels = /* @__PURE__ */ new Set();
15100
+ const terminalByChannel = /* @__PURE__ */ new WeakMap();
15101
+ const findCardContainer = (postId) => {
15102
+ for (const engine of engines.values()) {
15103
+ const name = engine.getCardContainerName(postId);
15104
+ if (name) return name;
15105
+ }
15106
+ return null;
15107
+ };
13365
15108
  const broadcastToAll = (msg) => {
13366
15109
  const frames = chunkWire(msg);
13367
15110
  for (const dc of openChannels) {
@@ -13469,12 +15212,45 @@ async function startDaemon(config) {
13469
15212
  }
13470
15213
  }
13471
15214
  };
15215
+ const sendRawToChannel = (frame) => {
15216
+ try {
15217
+ dc.sendMessage(frame);
15218
+ } catch {
15219
+ }
15220
+ };
15221
+ terminalByChannel.set(
15222
+ dc,
15223
+ new TerminalManager(sendRawToChannel, findCardContainer)
15224
+ );
15225
+ dc.onOpen(() => {
15226
+ for (const [spaceSlug, engine] of engines.entries()) {
15227
+ const sessions = engine.cardSessions();
15228
+ for (const summary of Object.values(sessions)) {
15229
+ const msg = JSON.stringify({
15230
+ type: "event",
15231
+ spaceSlug,
15232
+ event: `board-agent:${spaceSlug}:session-update`,
15233
+ payload: summary
15234
+ });
15235
+ sendToChannel(msg);
15236
+ }
15237
+ }
15238
+ });
13472
15239
  dc.onMessage((raw) => {
13473
15240
  const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
13474
15241
  const msgStr = reassembler.feed(rawStr);
13475
15242
  if (msgStr === null) return;
13476
15243
  try {
13477
15244
  const parsed = JSON.parse(msgStr);
15245
+ if (parsed.type === "terminal") {
15246
+ const mgr = terminalByChannel.get(dc);
15247
+ if (mgr) {
15248
+ mgr.handle(
15249
+ parsed
15250
+ );
15251
+ }
15252
+ return;
15253
+ }
13478
15254
  if (parsed.type === "broadcast" && parsed.spaceSlug && parsed.event) {
13479
15255
  const eventMsg = JSON.stringify({
13480
15256
  type: "event",
@@ -13506,6 +15282,11 @@ async function startDaemon(config) {
13506
15282
  dc.onClosed(() => {
13507
15283
  console.log(`Data channel closed with ${data.fromSocketId}`);
13508
15284
  openChannels.delete(dc);
15285
+ const mgr = terminalByChannel.get(dc);
15286
+ if (mgr) {
15287
+ mgr.destroyAll();
15288
+ terminalByChannel.delete(dc);
15289
+ }
13509
15290
  detachIceHandler();
13510
15291
  });
13511
15292
  });
@@ -13578,16 +15359,18 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
13578
15359
  }
13579
15360
  }
13580
15361
  function getDockerfilePath() {
13581
- return import_path4.default.join(__dirname, "..", "Dockerfile.claude");
15362
+ return import_path5.default.join(__dirname, "..", "Dockerfile.claude");
13582
15363
  }
13583
15364
 
13584
15365
  // src/setup.ts
13585
- var import_child_process6 = require("child_process");
13586
- var import_fs4 = __toESM(require("fs"));
13587
- var import_path5 = __toESM(require("path"));
15366
+ var import_child_process7 = require("child_process");
15367
+ var import_crypto2 = __toESM(require("crypto"));
15368
+ var import_fs5 = __toESM(require("fs"));
15369
+ var import_path6 = __toESM(require("path"));
15370
+ var DOCKERFILE_LABEL2 = "factiii.dockerfile-sha";
13588
15371
  function run(bin, args, opts) {
13589
15372
  try {
13590
- return (0, import_child_process6.execFileSync)(bin, args, {
15373
+ return (0, import_child_process7.execFileSync)(bin, args, {
13591
15374
  encoding: "utf-8",
13592
15375
  stdio: opts?.silent ? "pipe" : "inherit",
13593
15376
  // Avoids a cmd.exe window flash on Windows during silent checks.
@@ -13600,7 +15383,7 @@ function run(bin, args, opts) {
13600
15383
  function check(cmd) {
13601
15384
  const lookup2 = process.platform === "win32" ? "where" : "which";
13602
15385
  try {
13603
- (0, import_child_process6.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
15386
+ (0, import_child_process7.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
13604
15387
  return true;
13605
15388
  } catch {
13606
15389
  return false;
@@ -13630,21 +15413,32 @@ async function setup(serverUrl) {
13630
15413
  );
13631
15414
  process.exit(1);
13632
15415
  }
13633
- const imageExists = run(
15416
+ const wantedHash = import_crypto2.default.createHash("sha256").update(import_fs5.default.readFileSync(dockerfilePath)).digest("hex");
15417
+ const existingHash = run(
13634
15418
  "docker",
13635
- ["image", "inspect", "factiii-claude"],
15419
+ [
15420
+ "image",
15421
+ "inspect",
15422
+ "factiii-claude",
15423
+ "--format",
15424
+ `{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
15425
+ ],
13636
15426
  { silent: true }
13637
- );
13638
- if (imageExists) {
13639
- console.log(" Image already exists.");
15427
+ ).trim();
15428
+ if (existingHash === wantedHash) {
15429
+ console.log(" Image is up to date.");
13640
15430
  } else {
13641
- console.log(` Building from ${dockerfilePath}...`);
15431
+ console.log(
15432
+ existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
15433
+ );
13642
15434
  run("docker", [
13643
15435
  "build",
13644
15436
  "-t",
13645
15437
  "factiii-claude",
13646
15438
  "-f",
13647
15439
  dockerfilePath,
15440
+ "--label",
15441
+ `${DOCKERFILE_LABEL2}=${wantedHash}`,
13648
15442
  "."
13649
15443
  ]);
13650
15444
  console.log(" Image built.");
@@ -13658,13 +15452,14 @@ async function setup(serverUrl) {
13658
15452
  console.log("Start the runner with: factiii-runner start\n");
13659
15453
  }
13660
15454
  function findDockerfile() {
13661
- const p = import_path5.default.join(__dirname, "..", "Dockerfile.claude");
13662
- return import_fs4.default.existsSync(p) ? p : null;
15455
+ const p = import_path6.default.join(__dirname, "..", "Dockerfile.claude");
15456
+ return import_fs5.default.existsSync(p) ? p : null;
13663
15457
  }
13664
15458
 
13665
15459
  // src/cli.ts
15460
+ import_node_dns.default.setDefaultResultOrder("ipv4first");
13666
15461
  var program2 = new Command();
13667
- program2.name("factiii-runner").description("Factiii Runner \u2014 run Board AI agents on remote servers").version("0.0.1");
15462
+ program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version("0.0.1");
13668
15463
  program2.command("setup").description("Verify Docker, build the Claude image, and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
13669
15464
  try {
13670
15465
  await setup(opts.server);