@factiii/runner 0.0.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile.claude +25 -3
- package/dist/cli.js +2746 -885
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -963,8 +963,8 @@ var require_command = __commonJS({
|
|
|
963
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
|
|
967
|
-
var
|
|
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 =
|
|
1900
|
-
if (
|
|
1901
|
-
if (sourceExt.includes(
|
|
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) =>
|
|
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 =
|
|
1915
|
+
resolvedScriptPath = fs6.realpathSync(this._scriptPath);
|
|
1916
1916
|
} catch (err) {
|
|
1917
1917
|
resolvedScriptPath = this._scriptPath;
|
|
1918
1918
|
}
|
|
1919
|
-
executableDir =
|
|
1920
|
-
|
|
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 =
|
|
1927
|
+
const legacyName = path7.basename(
|
|
1928
1928
|
this._scriptPath,
|
|
1929
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
2795
|
-
if (
|
|
2796
|
-
this._executableDir =
|
|
2794
|
+
executableDir(path8) {
|
|
2795
|
+
if (path8 === void 0) return this._executableDir;
|
|
2796
|
+
this._executableDir = path8;
|
|
2797
2797
|
return this;
|
|
2798
2798
|
}
|
|
2799
2799
|
/**
|
|
@@ -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
|
|
3029
|
+
var fs6 = require("fs");
|
|
3030
3030
|
var Url = require("url");
|
|
3031
|
-
var
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
3327
|
+
var syncProc = spawn3(process.argv[0], ["-e", execString]);
|
|
3328
3328
|
var statusText;
|
|
3329
|
-
while (
|
|
3329
|
+
while (fs6.existsSync(syncFile)) {
|
|
3330
3330
|
}
|
|
3331
|
-
self.responseText =
|
|
3331
|
+
self.responseText = fs6.readFileSync(contentFile, "utf8");
|
|
3332
3332
|
syncProc.stdin.end();
|
|
3333
|
-
|
|
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
|
|
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 =
|
|
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
|
}
|
|
@@ -4260,15 +4260,16 @@ var require_src = __commonJS({
|
|
|
4260
4260
|
}
|
|
4261
4261
|
});
|
|
4262
4262
|
|
|
4263
|
-
// ../../node_modules/
|
|
4263
|
+
// ../../node_modules/ws/lib/constants.js
|
|
4264
4264
|
var require_constants = __commonJS({
|
|
4265
|
-
"../../node_modules/
|
|
4265
|
+
"../../node_modules/ws/lib/constants.js"(exports2, module2) {
|
|
4266
4266
|
"use strict";
|
|
4267
4267
|
var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
|
|
4268
4268
|
var hasBlob = typeof Blob !== "undefined";
|
|
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,
|
|
@@ -4282,9 +4283,9 @@ var require_constants = __commonJS({
|
|
|
4282
4283
|
}
|
|
4283
4284
|
});
|
|
4284
4285
|
|
|
4285
|
-
// ../../node_modules/
|
|
4286
|
+
// ../../node_modules/ws/lib/buffer-util.js
|
|
4286
4287
|
var require_buffer_util = __commonJS({
|
|
4287
|
-
"../../node_modules/
|
|
4288
|
+
"../../node_modules/ws/lib/buffer-util.js"(exports2, module2) {
|
|
4288
4289
|
"use strict";
|
|
4289
4290
|
var { EMPTY_BUFFER } = require_constants();
|
|
4290
4291
|
var FastBuffer = Buffer[Symbol.species];
|
|
@@ -4357,9 +4358,9 @@ var require_buffer_util = __commonJS({
|
|
|
4357
4358
|
}
|
|
4358
4359
|
});
|
|
4359
4360
|
|
|
4360
|
-
// ../../node_modules/
|
|
4361
|
+
// ../../node_modules/ws/lib/limiter.js
|
|
4361
4362
|
var require_limiter = __commonJS({
|
|
4362
|
-
"../../node_modules/
|
|
4363
|
+
"../../node_modules/ws/lib/limiter.js"(exports2, module2) {
|
|
4363
4364
|
"use strict";
|
|
4364
4365
|
var kDone = Symbol("kDone");
|
|
4365
4366
|
var kRun = Symbol("kRun");
|
|
@@ -4407,9 +4408,9 @@ var require_limiter = __commonJS({
|
|
|
4407
4408
|
}
|
|
4408
4409
|
});
|
|
4409
4410
|
|
|
4410
|
-
// ../../node_modules/
|
|
4411
|
+
// ../../node_modules/ws/lib/permessage-deflate.js
|
|
4411
4412
|
var require_permessage_deflate = __commonJS({
|
|
4412
|
-
"../../node_modules/
|
|
4413
|
+
"../../node_modules/ws/lib/permessage-deflate.js"(exports2, module2) {
|
|
4413
4414
|
"use strict";
|
|
4414
4415
|
var zlib = require("zlib");
|
|
4415
4416
|
var bufferUtil = require_buffer_util();
|
|
@@ -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
|
|
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
|
|
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.
|
|
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 =
|
|
4765
|
+
module2.exports = PerMessageDeflate2;
|
|
4765
4766
|
function deflateOnData(chunk) {
|
|
4766
4767
|
this[kBuffers].push(chunk);
|
|
4767
4768
|
this[kTotalLength] += chunk.length;
|
|
@@ -4790,9 +4791,9 @@ var require_permessage_deflate = __commonJS({
|
|
|
4790
4791
|
}
|
|
4791
4792
|
});
|
|
4792
4793
|
|
|
4793
|
-
// ../../node_modules/
|
|
4794
|
+
// ../../node_modules/ws/lib/validation.js
|
|
4794
4795
|
var require_validation = __commonJS({
|
|
4795
|
-
"../../node_modules/
|
|
4796
|
+
"../../node_modules/ws/lib/validation.js"(exports2, module2) {
|
|
4796
4797
|
"use strict";
|
|
4797
4798
|
var { isUtf8 } = require("buffer");
|
|
4798
4799
|
var { hasBlob } = require_constants();
|
|
@@ -4991,12 +4992,12 @@ var require_validation = __commonJS({
|
|
|
4991
4992
|
}
|
|
4992
4993
|
});
|
|
4993
4994
|
|
|
4994
|
-
// ../../node_modules/
|
|
4995
|
+
// ../../node_modules/ws/lib/receiver.js
|
|
4995
4996
|
var require_receiver = __commonJS({
|
|
4996
|
-
"../../node_modules/
|
|
4997
|
+
"../../node_modules/ws/lib/receiver.js"(exports2, module2) {
|
|
4997
4998
|
"use strict";
|
|
4998
4999
|
var { Writable } = require("stream");
|
|
4999
|
-
var
|
|
5000
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5000
5001
|
var {
|
|
5001
5002
|
BINARY_TYPES,
|
|
5002
5003
|
EMPTY_BUFFER,
|
|
@@ -5026,6 +5027,10 @@ var require_receiver = __commonJS({
|
|
|
5026
5027
|
* extensions
|
|
5027
5028
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
|
5028
5029
|
* client or server mode
|
|
5030
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
5031
|
+
* buffered data chunks
|
|
5032
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
5033
|
+
* fragments
|
|
5029
5034
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
5030
5035
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
5031
5036
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -5036,6 +5041,8 @@ var require_receiver = __commonJS({
|
|
|
5036
5041
|
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
|
5037
5042
|
this._extensions = options.extensions || {};
|
|
5038
5043
|
this._isServer = !!options.isServer;
|
|
5044
|
+
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
|
5045
|
+
this._maxFragments = options.maxFragments | 0;
|
|
5039
5046
|
this._maxPayload = options.maxPayload | 0;
|
|
5040
5047
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
|
5041
5048
|
this[kWebSocket] = void 0;
|
|
@@ -5065,6 +5072,18 @@ var require_receiver = __commonJS({
|
|
|
5065
5072
|
*/
|
|
5066
5073
|
_write(chunk, encoding, cb) {
|
|
5067
5074
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
5075
|
+
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
5076
|
+
cb(
|
|
5077
|
+
this.createError(
|
|
5078
|
+
RangeError,
|
|
5079
|
+
"Too many buffered chunks",
|
|
5080
|
+
false,
|
|
5081
|
+
1008,
|
|
5082
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
5083
|
+
)
|
|
5084
|
+
);
|
|
5085
|
+
return;
|
|
5086
|
+
}
|
|
5068
5087
|
this._bufferedBytes += chunk.length;
|
|
5069
5088
|
this._buffers.push(chunk);
|
|
5070
5089
|
this.startLoop(cb);
|
|
@@ -5163,7 +5182,7 @@ var require_receiver = __commonJS({
|
|
|
5163
5182
|
return;
|
|
5164
5183
|
}
|
|
5165
5184
|
const compressed = (buf[0] & 64) === 64;
|
|
5166
|
-
if (compressed && !this._extensions[
|
|
5185
|
+
if (compressed && !this._extensions[PerMessageDeflate2.extensionName]) {
|
|
5167
5186
|
const error = this.createError(
|
|
5168
5187
|
RangeError,
|
|
5169
5188
|
"RSV1 must be clear",
|
|
@@ -5394,6 +5413,17 @@ var require_receiver = __commonJS({
|
|
|
5394
5413
|
return;
|
|
5395
5414
|
}
|
|
5396
5415
|
if (data.length) {
|
|
5416
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
5417
|
+
const error = this.createError(
|
|
5418
|
+
RangeError,
|
|
5419
|
+
"Too many message fragments",
|
|
5420
|
+
false,
|
|
5421
|
+
1008,
|
|
5422
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
5423
|
+
);
|
|
5424
|
+
cb(error);
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5397
5427
|
this._messageLength = this._totalPayloadLength;
|
|
5398
5428
|
this._fragments.push(data);
|
|
5399
5429
|
}
|
|
@@ -5407,7 +5437,7 @@ var require_receiver = __commonJS({
|
|
|
5407
5437
|
* @private
|
|
5408
5438
|
*/
|
|
5409
5439
|
decompress(data, cb) {
|
|
5410
|
-
const perMessageDeflate = this._extensions[
|
|
5440
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
5411
5441
|
perMessageDeflate.decompress(data, this._fin, (err, buf) => {
|
|
5412
5442
|
if (err) return cb(err);
|
|
5413
5443
|
if (buf.length) {
|
|
@@ -5423,6 +5453,17 @@ var require_receiver = __commonJS({
|
|
|
5423
5453
|
cb(error);
|
|
5424
5454
|
return;
|
|
5425
5455
|
}
|
|
5456
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
5457
|
+
const error = this.createError(
|
|
5458
|
+
RangeError,
|
|
5459
|
+
"Too many message fragments",
|
|
5460
|
+
false,
|
|
5461
|
+
1008,
|
|
5462
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
5463
|
+
);
|
|
5464
|
+
cb(error);
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5426
5467
|
this._fragments.push(buf);
|
|
5427
5468
|
}
|
|
5428
5469
|
this.dataMessage(cb);
|
|
@@ -5583,13 +5624,16 @@ var require_receiver = __commonJS({
|
|
|
5583
5624
|
}
|
|
5584
5625
|
});
|
|
5585
5626
|
|
|
5586
|
-
// ../../node_modules/
|
|
5627
|
+
// ../../node_modules/ws/lib/sender.js
|
|
5587
5628
|
var require_sender = __commonJS({
|
|
5588
|
-
"../../node_modules/
|
|
5629
|
+
"../../node_modules/ws/lib/sender.js"(exports2, module2) {
|
|
5589
5630
|
"use strict";
|
|
5590
5631
|
var { Duplex } = require("stream");
|
|
5591
5632
|
var { randomFillSync } = require("crypto");
|
|
5592
|
-
var
|
|
5633
|
+
var {
|
|
5634
|
+
types: { isUint8Array }
|
|
5635
|
+
} = require("util");
|
|
5636
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5593
5637
|
var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
|
|
5594
5638
|
var { isBlob, isValidStatusCode } = require_validation();
|
|
5595
5639
|
var { mask: applyMask, toBuffer: toBuffer2 } = require_buffer_util();
|
|
@@ -5742,8 +5786,10 @@ var require_sender = __commonJS({
|
|
|
5742
5786
|
buf.writeUInt16BE(code, 0);
|
|
5743
5787
|
if (typeof data === "string") {
|
|
5744
5788
|
buf.write(data, 2);
|
|
5745
|
-
} else {
|
|
5789
|
+
} else if (isUint8Array(data)) {
|
|
5746
5790
|
buf.set(data, 2);
|
|
5791
|
+
} else {
|
|
5792
|
+
throw new TypeError("Second argument must be a string or a Uint8Array");
|
|
5747
5793
|
}
|
|
5748
5794
|
}
|
|
5749
5795
|
const options = {
|
|
@@ -5873,7 +5919,7 @@ var require_sender = __commonJS({
|
|
|
5873
5919
|
* @public
|
|
5874
5920
|
*/
|
|
5875
5921
|
send(data, options, cb) {
|
|
5876
|
-
const perMessageDeflate = this._extensions[
|
|
5922
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
5877
5923
|
let opcode = options.binary ? 2 : 1;
|
|
5878
5924
|
let rsv1 = options.compress;
|
|
5879
5925
|
let byteLength2;
|
|
@@ -5997,7 +6043,7 @@ var require_sender = __commonJS({
|
|
|
5997
6043
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
5998
6044
|
return;
|
|
5999
6045
|
}
|
|
6000
|
-
const perMessageDeflate = this._extensions[
|
|
6046
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
6001
6047
|
this._bufferedBytes += options[kByteLength];
|
|
6002
6048
|
this._state = DEFLATING;
|
|
6003
6049
|
perMessageDeflate.compress(data, options.fin, (_, buf) => {
|
|
@@ -6071,9 +6117,9 @@ var require_sender = __commonJS({
|
|
|
6071
6117
|
}
|
|
6072
6118
|
});
|
|
6073
6119
|
|
|
6074
|
-
// ../../node_modules/
|
|
6120
|
+
// ../../node_modules/ws/lib/event-target.js
|
|
6075
6121
|
var require_event_target = __commonJS({
|
|
6076
|
-
"../../node_modules/
|
|
6122
|
+
"../../node_modules/ws/lib/event-target.js"(exports2, module2) {
|
|
6077
6123
|
"use strict";
|
|
6078
6124
|
var { kForOnEventAttribute, kListener } = require_constants();
|
|
6079
6125
|
var kCode = Symbol("kCode");
|
|
@@ -6300,9 +6346,9 @@ var require_event_target = __commonJS({
|
|
|
6300
6346
|
}
|
|
6301
6347
|
});
|
|
6302
6348
|
|
|
6303
|
-
// ../../node_modules/
|
|
6349
|
+
// ../../node_modules/ws/lib/extension.js
|
|
6304
6350
|
var require_extension = __commonJS({
|
|
6305
|
-
"../../node_modules/
|
|
6351
|
+
"../../node_modules/ws/lib/extension.js"(exports2, module2) {
|
|
6306
6352
|
"use strict";
|
|
6307
6353
|
var { tokenChars } = require_validation();
|
|
6308
6354
|
function push(dest, name, elem) {
|
|
@@ -6435,11 +6481,11 @@ var require_extension = __commonJS({
|
|
|
6435
6481
|
return offers;
|
|
6436
6482
|
}
|
|
6437
6483
|
function format(extensions) {
|
|
6438
|
-
return Object.keys(extensions).map((
|
|
6439
|
-
let configurations = extensions[
|
|
6484
|
+
return Object.keys(extensions).map((extension2) => {
|
|
6485
|
+
let configurations = extensions[extension2];
|
|
6440
6486
|
if (!Array.isArray(configurations)) configurations = [configurations];
|
|
6441
6487
|
return configurations.map((params) => {
|
|
6442
|
-
return [
|
|
6488
|
+
return [extension2].concat(
|
|
6443
6489
|
Object.keys(params).map((k) => {
|
|
6444
6490
|
let values = params[k];
|
|
6445
6491
|
if (!Array.isArray(values)) values = [values];
|
|
@@ -6453,9 +6499,9 @@ var require_extension = __commonJS({
|
|
|
6453
6499
|
}
|
|
6454
6500
|
});
|
|
6455
6501
|
|
|
6456
|
-
// ../../node_modules/
|
|
6502
|
+
// ../../node_modules/ws/lib/websocket.js
|
|
6457
6503
|
var require_websocket = __commonJS({
|
|
6458
|
-
"../../node_modules/
|
|
6504
|
+
"../../node_modules/ws/lib/websocket.js"(exports2, module2) {
|
|
6459
6505
|
"use strict";
|
|
6460
6506
|
var EventEmitter = require("events");
|
|
6461
6507
|
var https = require("https");
|
|
@@ -6465,12 +6511,13 @@ var require_websocket = __commonJS({
|
|
|
6465
6511
|
var { randomBytes, createHash } = require("crypto");
|
|
6466
6512
|
var { Duplex, Readable } = require("stream");
|
|
6467
6513
|
var { URL: URL2 } = require("url");
|
|
6468
|
-
var
|
|
6514
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
6469
6515
|
var Receiver2 = require_receiver();
|
|
6470
6516
|
var Sender2 = require_sender();
|
|
6471
6517
|
var { isBlob } = require_validation();
|
|
6472
6518
|
var {
|
|
6473
6519
|
BINARY_TYPES,
|
|
6520
|
+
CLOSE_TIMEOUT,
|
|
6474
6521
|
EMPTY_BUFFER,
|
|
6475
6522
|
GUID,
|
|
6476
6523
|
kForOnEventAttribute,
|
|
@@ -6484,7 +6531,6 @@ var require_websocket = __commonJS({
|
|
|
6484
6531
|
} = require_event_target();
|
|
6485
6532
|
var { format, parse: parse3 } = require_extension();
|
|
6486
6533
|
var { toBuffer: toBuffer2 } = require_buffer_util();
|
|
6487
|
-
var closeTimeout = 30 * 1e3;
|
|
6488
6534
|
var kAborted = Symbol("kAborted");
|
|
6489
6535
|
var protocolVersions = [8, 13];
|
|
6490
6536
|
var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
@@ -6530,6 +6576,7 @@ var require_websocket = __commonJS({
|
|
|
6530
6576
|
initAsClient(this, address, protocols, options);
|
|
6531
6577
|
} else {
|
|
6532
6578
|
this._autoPong = options.autoPong;
|
|
6579
|
+
this._closeTimeout = options.closeTimeout;
|
|
6533
6580
|
this._isServer = true;
|
|
6534
6581
|
}
|
|
6535
6582
|
}
|
|
@@ -6623,6 +6670,10 @@ var require_websocket = __commonJS({
|
|
|
6623
6670
|
* multiple times in the same tick
|
|
6624
6671
|
* @param {Function} [options.generateMask] The function used to generate the
|
|
6625
6672
|
* masking key
|
|
6673
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
6674
|
+
* buffered data chunks
|
|
6675
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
6676
|
+
* fragments
|
|
6626
6677
|
* @param {Number} [options.maxPayload=0] The maximum allowed message size
|
|
6627
6678
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
6628
6679
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -6634,6 +6685,8 @@ var require_websocket = __commonJS({
|
|
|
6634
6685
|
binaryType: this.binaryType,
|
|
6635
6686
|
extensions: this._extensions,
|
|
6636
6687
|
isServer: this._isServer,
|
|
6688
|
+
maxBufferedChunks: options.maxBufferedChunks,
|
|
6689
|
+
maxFragments: options.maxFragments,
|
|
6637
6690
|
maxPayload: options.maxPayload,
|
|
6638
6691
|
skipUTF8Validation: options.skipUTF8Validation
|
|
6639
6692
|
});
|
|
@@ -6672,8 +6725,8 @@ var require_websocket = __commonJS({
|
|
|
6672
6725
|
this.emit("close", this._closeCode, this._closeMessage);
|
|
6673
6726
|
return;
|
|
6674
6727
|
}
|
|
6675
|
-
if (this._extensions[
|
|
6676
|
-
this._extensions[
|
|
6728
|
+
if (this._extensions[PerMessageDeflate2.extensionName]) {
|
|
6729
|
+
this._extensions[PerMessageDeflate2.extensionName].cleanup();
|
|
6677
6730
|
}
|
|
6678
6731
|
this._receiver.removeAllListeners();
|
|
6679
6732
|
this._readyState = _WebSocket.CLOSED;
|
|
@@ -6835,7 +6888,7 @@ var require_websocket = __commonJS({
|
|
|
6835
6888
|
fin: true,
|
|
6836
6889
|
...options
|
|
6837
6890
|
};
|
|
6838
|
-
if (!this._extensions[
|
|
6891
|
+
if (!this._extensions[PerMessageDeflate2.extensionName]) {
|
|
6839
6892
|
opts.compress = false;
|
|
6840
6893
|
}
|
|
6841
6894
|
this._sender.send(data || EMPTY_BUFFER, opts, cb);
|
|
@@ -6931,7 +6984,10 @@ var require_websocket = __commonJS({
|
|
|
6931
6984
|
const opts = {
|
|
6932
6985
|
allowSynchronousEvents: true,
|
|
6933
6986
|
autoPong: true,
|
|
6987
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
6934
6988
|
protocolVersion: protocolVersions[1],
|
|
6989
|
+
maxBufferedChunks: 1024 * 1024,
|
|
6990
|
+
maxFragments: 128 * 1024,
|
|
6935
6991
|
maxPayload: 100 * 1024 * 1024,
|
|
6936
6992
|
skipUTF8Validation: false,
|
|
6937
6993
|
perMessageDeflate: true,
|
|
@@ -6948,6 +7004,7 @@ var require_websocket = __commonJS({
|
|
|
6948
7004
|
port: void 0
|
|
6949
7005
|
};
|
|
6950
7006
|
websocket._autoPong = opts.autoPong;
|
|
7007
|
+
websocket._closeTimeout = opts.closeTimeout;
|
|
6951
7008
|
if (!protocolVersions.includes(opts.protocolVersion)) {
|
|
6952
7009
|
throw new RangeError(
|
|
6953
7010
|
`Unsupported protocol version: ${opts.protocolVersion} (supported versions: ${protocolVersions.join(", ")})`
|
|
@@ -6959,7 +7016,7 @@ var require_websocket = __commonJS({
|
|
|
6959
7016
|
} else {
|
|
6960
7017
|
try {
|
|
6961
7018
|
parsedUrl = new URL2(address);
|
|
6962
|
-
} catch
|
|
7019
|
+
} catch {
|
|
6963
7020
|
throw new SyntaxError(`Invalid URL: ${address}`);
|
|
6964
7021
|
}
|
|
6965
7022
|
}
|
|
@@ -7007,13 +7064,13 @@ var require_websocket = __commonJS({
|
|
|
7007
7064
|
opts.path = parsedUrl.pathname + parsedUrl.search;
|
|
7008
7065
|
opts.timeout = opts.handshakeTimeout;
|
|
7009
7066
|
if (opts.perMessageDeflate) {
|
|
7010
|
-
perMessageDeflate = new
|
|
7011
|
-
opts.perMessageDeflate
|
|
7012
|
-
false,
|
|
7013
|
-
opts.maxPayload
|
|
7014
|
-
);
|
|
7067
|
+
perMessageDeflate = new PerMessageDeflate2({
|
|
7068
|
+
...opts.perMessageDeflate,
|
|
7069
|
+
isServer: false,
|
|
7070
|
+
maxPayload: opts.maxPayload
|
|
7071
|
+
});
|
|
7015
7072
|
opts.headers["Sec-WebSocket-Extensions"] = format({
|
|
7016
|
-
[
|
|
7073
|
+
[PerMessageDeflate2.extensionName]: perMessageDeflate.offer()
|
|
7017
7074
|
});
|
|
7018
7075
|
}
|
|
7019
7076
|
if (protocols.length) {
|
|
@@ -7156,23 +7213,25 @@ var require_websocket = __commonJS({
|
|
|
7156
7213
|
return;
|
|
7157
7214
|
}
|
|
7158
7215
|
const extensionNames = Object.keys(extensions);
|
|
7159
|
-
if (extensionNames.length !== 1 || extensionNames[0] !==
|
|
7216
|
+
if (extensionNames.length !== 1 || extensionNames[0] !== PerMessageDeflate2.extensionName) {
|
|
7160
7217
|
const message = "Server indicated an extension that was not requested";
|
|
7161
7218
|
abortHandshake(websocket, socket, message);
|
|
7162
7219
|
return;
|
|
7163
7220
|
}
|
|
7164
7221
|
try {
|
|
7165
|
-
perMessageDeflate.accept(extensions[
|
|
7222
|
+
perMessageDeflate.accept(extensions[PerMessageDeflate2.extensionName]);
|
|
7166
7223
|
} catch (err) {
|
|
7167
7224
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
7168
7225
|
abortHandshake(websocket, socket, message);
|
|
7169
7226
|
return;
|
|
7170
7227
|
}
|
|
7171
|
-
websocket._extensions[
|
|
7228
|
+
websocket._extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
|
|
7172
7229
|
}
|
|
7173
7230
|
websocket.setSocket(socket, head, {
|
|
7174
7231
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
|
7175
7232
|
generateMask: opts.generateMask,
|
|
7233
|
+
maxBufferedChunks: opts.maxBufferedChunks,
|
|
7234
|
+
maxFragments: opts.maxFragments,
|
|
7176
7235
|
maxPayload: opts.maxPayload,
|
|
7177
7236
|
skipUTF8Validation: opts.skipUTF8Validation
|
|
7178
7237
|
});
|
|
@@ -7290,7 +7349,7 @@ var require_websocket = __commonJS({
|
|
|
7290
7349
|
function setCloseTimer(websocket) {
|
|
7291
7350
|
websocket._closeTimer = setTimeout(
|
|
7292
7351
|
websocket._socket.destroy.bind(websocket._socket),
|
|
7293
|
-
|
|
7352
|
+
websocket._closeTimeout
|
|
7294
7353
|
);
|
|
7295
7354
|
}
|
|
7296
7355
|
function socketOnClose() {
|
|
@@ -7299,8 +7358,8 @@ var require_websocket = __commonJS({
|
|
|
7299
7358
|
this.removeListener("data", socketOnData);
|
|
7300
7359
|
this.removeListener("end", socketOnEnd);
|
|
7301
7360
|
websocket._readyState = WebSocket2.CLOSING;
|
|
7302
|
-
|
|
7303
|
-
|
|
7361
|
+
if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
|
|
7362
|
+
const chunk = this.read(this._readableState.length);
|
|
7304
7363
|
websocket._receiver.write(chunk);
|
|
7305
7364
|
}
|
|
7306
7365
|
websocket._receiver.end();
|
|
@@ -7336,9 +7395,9 @@ var require_websocket = __commonJS({
|
|
|
7336
7395
|
}
|
|
7337
7396
|
});
|
|
7338
7397
|
|
|
7339
|
-
// ../../node_modules/
|
|
7398
|
+
// ../../node_modules/ws/lib/stream.js
|
|
7340
7399
|
var require_stream = __commonJS({
|
|
7341
|
-
"../../node_modules/
|
|
7400
|
+
"../../node_modules/ws/lib/stream.js"(exports2, module2) {
|
|
7342
7401
|
"use strict";
|
|
7343
7402
|
var WebSocket2 = require_websocket();
|
|
7344
7403
|
var { Duplex } = require("stream");
|
|
@@ -7434,9 +7493,9 @@ var require_stream = __commonJS({
|
|
|
7434
7493
|
}
|
|
7435
7494
|
});
|
|
7436
7495
|
|
|
7437
|
-
// ../../node_modules/
|
|
7496
|
+
// ../../node_modules/ws/lib/subprotocol.js
|
|
7438
7497
|
var require_subprotocol = __commonJS({
|
|
7439
|
-
"../../node_modules/
|
|
7498
|
+
"../../node_modules/ws/lib/subprotocol.js"(exports2, module2) {
|
|
7440
7499
|
"use strict";
|
|
7441
7500
|
var { tokenChars } = require_validation();
|
|
7442
7501
|
function parse3(header) {
|
|
@@ -7479,19 +7538,19 @@ var require_subprotocol = __commonJS({
|
|
|
7479
7538
|
}
|
|
7480
7539
|
});
|
|
7481
7540
|
|
|
7482
|
-
// ../../node_modules/
|
|
7541
|
+
// ../../node_modules/ws/lib/websocket-server.js
|
|
7483
7542
|
var require_websocket_server = __commonJS({
|
|
7484
|
-
"../../node_modules/
|
|
7543
|
+
"../../node_modules/ws/lib/websocket-server.js"(exports2, module2) {
|
|
7485
7544
|
"use strict";
|
|
7486
7545
|
var EventEmitter = require("events");
|
|
7487
7546
|
var http = require("http");
|
|
7488
7547
|
var { Duplex } = require("stream");
|
|
7489
7548
|
var { createHash } = require("crypto");
|
|
7490
|
-
var
|
|
7491
|
-
var
|
|
7492
|
-
var
|
|
7549
|
+
var extension2 = require_extension();
|
|
7550
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
7551
|
+
var subprotocol2 = require_subprotocol();
|
|
7493
7552
|
var WebSocket2 = require_websocket();
|
|
7494
|
-
var { GUID, kWebSocket } = require_constants();
|
|
7553
|
+
var { CLOSE_TIMEOUT, GUID, kWebSocket } = require_constants();
|
|
7495
7554
|
var keyRegex = /^[+/0-9A-Za-z]{22}==$/;
|
|
7496
7555
|
var RUNNING = 0;
|
|
7497
7556
|
var CLOSING = 1;
|
|
@@ -7510,8 +7569,15 @@ var require_websocket_server = __commonJS({
|
|
|
7510
7569
|
* pending connections
|
|
7511
7570
|
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
|
|
7512
7571
|
* track clients
|
|
7572
|
+
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
|
|
7573
|
+
* wait for the closing handshake to finish after `websocket.close()` is
|
|
7574
|
+
* called
|
|
7513
7575
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
7514
7576
|
* @param {String} [options.host] The hostname where to bind the server
|
|
7577
|
+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
|
|
7578
|
+
* buffered data chunks
|
|
7579
|
+
* @param {Number} [options.maxFragments=131072] The maximum number of message
|
|
7580
|
+
* fragments
|
|
7515
7581
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
7516
7582
|
* size
|
|
7517
7583
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
|
@@ -7533,11 +7599,14 @@ var require_websocket_server = __commonJS({
|
|
|
7533
7599
|
options = {
|
|
7534
7600
|
allowSynchronousEvents: true,
|
|
7535
7601
|
autoPong: true,
|
|
7602
|
+
maxBufferedChunks: 1024 * 1024,
|
|
7603
|
+
maxFragments: 128 * 1024,
|
|
7536
7604
|
maxPayload: 100 * 1024 * 1024,
|
|
7537
7605
|
skipUTF8Validation: false,
|
|
7538
7606
|
perMessageDeflate: false,
|
|
7539
7607
|
handleProtocols: null,
|
|
7540
7608
|
clientTracking: true,
|
|
7609
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
7541
7610
|
verifyClient: null,
|
|
7542
7611
|
noServer: false,
|
|
7543
7612
|
backlog: null,
|
|
@@ -7708,7 +7777,7 @@ var require_websocket_server = __commonJS({
|
|
|
7708
7777
|
let protocols = /* @__PURE__ */ new Set();
|
|
7709
7778
|
if (secWebSocketProtocol !== void 0) {
|
|
7710
7779
|
try {
|
|
7711
|
-
protocols =
|
|
7780
|
+
protocols = subprotocol2.parse(secWebSocketProtocol);
|
|
7712
7781
|
} catch (err) {
|
|
7713
7782
|
const message = "Invalid Sec-WebSocket-Protocol header";
|
|
7714
7783
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
|
@@ -7718,16 +7787,16 @@ var require_websocket_server = __commonJS({
|
|
|
7718
7787
|
const secWebSocketExtensions = req.headers["sec-websocket-extensions"];
|
|
7719
7788
|
const extensions = {};
|
|
7720
7789
|
if (this.options.perMessageDeflate && secWebSocketExtensions !== void 0) {
|
|
7721
|
-
const perMessageDeflate = new
|
|
7722
|
-
this.options.perMessageDeflate,
|
|
7723
|
-
true,
|
|
7724
|
-
this.options.maxPayload
|
|
7725
|
-
);
|
|
7790
|
+
const perMessageDeflate = new PerMessageDeflate2({
|
|
7791
|
+
...this.options.perMessageDeflate,
|
|
7792
|
+
isServer: true,
|
|
7793
|
+
maxPayload: this.options.maxPayload
|
|
7794
|
+
});
|
|
7726
7795
|
try {
|
|
7727
|
-
const offers =
|
|
7728
|
-
if (offers[
|
|
7729
|
-
perMessageDeflate.accept(offers[
|
|
7730
|
-
extensions[
|
|
7796
|
+
const offers = extension2.parse(secWebSocketExtensions);
|
|
7797
|
+
if (offers[PerMessageDeflate2.extensionName]) {
|
|
7798
|
+
perMessageDeflate.accept(offers[PerMessageDeflate2.extensionName]);
|
|
7799
|
+
extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
|
|
7731
7800
|
}
|
|
7732
7801
|
} catch (err) {
|
|
7733
7802
|
const message = "Invalid or unacceptable Sec-WebSocket-Extensions header";
|
|
@@ -7798,10 +7867,10 @@ var require_websocket_server = __commonJS({
|
|
|
7798
7867
|
ws._protocol = protocol4;
|
|
7799
7868
|
}
|
|
7800
7869
|
}
|
|
7801
|
-
if (extensions[
|
|
7802
|
-
const params = extensions[
|
|
7803
|
-
const value2 =
|
|
7804
|
-
[
|
|
7870
|
+
if (extensions[PerMessageDeflate2.extensionName]) {
|
|
7871
|
+
const params = extensions[PerMessageDeflate2.extensionName].params;
|
|
7872
|
+
const value2 = extension2.format({
|
|
7873
|
+
[PerMessageDeflate2.extensionName]: [params]
|
|
7805
7874
|
});
|
|
7806
7875
|
headers.push(`Sec-WebSocket-Extensions: ${value2}`);
|
|
7807
7876
|
ws._extensions = extensions;
|
|
@@ -7811,6 +7880,8 @@ var require_websocket_server = __commonJS({
|
|
|
7811
7880
|
socket.removeListener("error", socketOnError);
|
|
7812
7881
|
ws.setSocket(socket, head, {
|
|
7813
7882
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
|
7883
|
+
maxBufferedChunks: this.options.maxBufferedChunks,
|
|
7884
|
+
maxFragments: this.options.maxFragments,
|
|
7814
7885
|
maxPayload: this.options.maxPayload,
|
|
7815
7886
|
skipUTF8Validation: this.options.skipUTF8Validation
|
|
7816
7887
|
});
|
|
@@ -7868,6 +7939,9 @@ var require_websocket_server = __commonJS({
|
|
|
7868
7939
|
}
|
|
7869
7940
|
});
|
|
7870
7941
|
|
|
7942
|
+
// src/cli.ts
|
|
7943
|
+
var import_node_dns = __toESM(require("node:dns"));
|
|
7944
|
+
|
|
7871
7945
|
// node_modules/commander/esm.mjs
|
|
7872
7946
|
var import_index = __toESM(require_commander(), 1);
|
|
7873
7947
|
var {
|
|
@@ -7963,10 +8037,10 @@ function configExists() {
|
|
|
7963
8037
|
return import_fs.default.existsSync(CONFIG_FILE);
|
|
7964
8038
|
}
|
|
7965
8039
|
|
|
7966
|
-
// ../../shared/all/helpers/
|
|
8040
|
+
// ../../shared/all/helpers/board-agent-core/claude.ts
|
|
7967
8041
|
var import_child_process2 = require("child_process");
|
|
7968
8042
|
|
|
7969
|
-
// ../../shared/all/helpers/
|
|
8043
|
+
// ../../shared/all/helpers/board-agent-core/claude-stream.ts
|
|
7970
8044
|
function truncate(s, max) {
|
|
7971
8045
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
7972
8046
|
}
|
|
@@ -8194,6 +8268,12 @@ function pipeClaudeStream(proc, callbacks) {
|
|
|
8194
8268
|
for (const line of lines) {
|
|
8195
8269
|
const trimmed = line.trim();
|
|
8196
8270
|
if (!trimmed) continue;
|
|
8271
|
+
if (trimmed[0] !== "{" && trimmed[0] !== "[") {
|
|
8272
|
+
state.isError = true;
|
|
8273
|
+
state.fullText = state.fullText ? `${state.fullText}
|
|
8274
|
+
${trimmed}` : trimmed;
|
|
8275
|
+
continue;
|
|
8276
|
+
}
|
|
8197
8277
|
try {
|
|
8198
8278
|
const raw = JSON.parse(trimmed);
|
|
8199
8279
|
if (processStreamEvent(raw, state, callbacks)) continue;
|
|
@@ -8221,7 +8301,7 @@ function pipeClaudeStream(proc, callbacks) {
|
|
|
8221
8301
|
};
|
|
8222
8302
|
}
|
|
8223
8303
|
|
|
8224
|
-
// ../../shared/all/helpers/
|
|
8304
|
+
// ../../shared/all/helpers/board-agent-core/prompts.json
|
|
8225
8305
|
var prompts_default = {
|
|
8226
8306
|
system: [
|
|
8227
8307
|
"You are a coding agent working inside a Docker container on a task from a project management board.",
|
|
@@ -8231,7 +8311,7 @@ var prompts_default = {
|
|
|
8231
8311
|
"2. **Implement** \u2014 Execute the plan. You gain Edit, Write, and Bash tools. Make the code changes.",
|
|
8232
8312
|
"3. **Review** \u2014 Validate your changes, fix issues, generate reproduction steps for the developer.",
|
|
8233
8313
|
"",
|
|
8234
|
-
"
|
|
8314
|
+
"After Review, the developer may request an optional **Preview** where you set up a live dev environment on demand.",
|
|
8235
8315
|
"",
|
|
8236
8316
|
"## Task Tracking",
|
|
8237
8317
|
"Use the TodoWrite tool to track your progress. Create todos at the start of each phase with your planned steps,",
|
|
@@ -8394,7 +8474,7 @@ var prompts_default = {
|
|
|
8394
8474
|
commitMessage: 'Generate a single-line conventional commit message (e.g. "feat: ...", "fix: ...", "refactor: ...") for these changes. Output ONLY the commit message, nothing else.'
|
|
8395
8475
|
};
|
|
8396
8476
|
|
|
8397
|
-
// ../../shared/all/helpers/
|
|
8477
|
+
// ../../shared/all/helpers/board-agent-core/claude.ts
|
|
8398
8478
|
function spawnClaude(opts) {
|
|
8399
8479
|
const args = [
|
|
8400
8480
|
"exec",
|
|
@@ -8494,11 +8574,18 @@ ${diff.slice(0, 8e3)}`);
|
|
|
8494
8574
|
});
|
|
8495
8575
|
}
|
|
8496
8576
|
|
|
8497
|
-
// ../../shared/all/helpers/
|
|
8577
|
+
// ../../shared/all/helpers/board-agent-core/docker.ts
|
|
8498
8578
|
var import_child_process3 = require("child_process");
|
|
8579
|
+
var import_crypto = __toESM(require("crypto"));
|
|
8580
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
8499
8581
|
var import_net = __toESM(require("net"));
|
|
8500
8582
|
var import_util = require("util");
|
|
8501
8583
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
8584
|
+
var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
|
|
8585
|
+
async function hashDockerfile(dockerfilePath) {
|
|
8586
|
+
const buf = await import_promises.default.readFile(dockerfilePath);
|
|
8587
|
+
return import_crypto.default.createHash("sha256").update(buf).digest("hex");
|
|
8588
|
+
}
|
|
8502
8589
|
async function getFreePort() {
|
|
8503
8590
|
return new Promise((resolve, reject) => {
|
|
8504
8591
|
const srv = import_net.default.createServer();
|
|
@@ -8524,28 +8611,105 @@ async function assertDockerRunning() {
|
|
|
8524
8611
|
);
|
|
8525
8612
|
}
|
|
8526
8613
|
}
|
|
8527
|
-
|
|
8614
|
+
var BUILD_TIMEOUT_MS = 9e5;
|
|
8615
|
+
async function ensureDockerImage(dockerfilePath, onLog) {
|
|
8528
8616
|
await assertDockerRunning();
|
|
8617
|
+
const wantedHash = await hashDockerfile(dockerfilePath);
|
|
8529
8618
|
try {
|
|
8530
|
-
|
|
8531
|
-
|
|
8619
|
+
const { stdout } = await execFileAsync(
|
|
8620
|
+
"docker",
|
|
8621
|
+
[
|
|
8622
|
+
"image",
|
|
8623
|
+
"inspect",
|
|
8624
|
+
"factiii-claude",
|
|
8625
|
+
"--format",
|
|
8626
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
|
|
8627
|
+
],
|
|
8628
|
+
{}
|
|
8629
|
+
);
|
|
8630
|
+
if (stdout.trim() === wantedHash) return;
|
|
8532
8631
|
} catch {
|
|
8533
8632
|
}
|
|
8534
|
-
await
|
|
8535
|
-
|
|
8536
|
-
["build", "-t", "factiii-claude", "-f", dockerfilePath, "."],
|
|
8537
|
-
{ timeout: 3e5 }
|
|
8538
|
-
);
|
|
8633
|
+
await buildImageStreaming(dockerfilePath, wantedHash, onLog);
|
|
8634
|
+
await resetNixVolume();
|
|
8539
8635
|
}
|
|
8540
|
-
async function
|
|
8636
|
+
async function resetNixVolume() {
|
|
8541
8637
|
try {
|
|
8542
|
-
await execFileAsync(
|
|
8638
|
+
const { stdout } = await execFileAsync("docker", [
|
|
8639
|
+
"ps",
|
|
8640
|
+
"-aq",
|
|
8641
|
+
"--filter",
|
|
8642
|
+
"volume=factiii-nix"
|
|
8643
|
+
]);
|
|
8644
|
+
const ids = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
8645
|
+
if (ids.length) await execFileAsync("docker", ["rm", "-f", ...ids]);
|
|
8646
|
+
await execFileAsync("docker", ["volume", "rm", "factiii-nix"]);
|
|
8647
|
+
} catch {
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
|
|
8651
|
+
return new Promise((resolve, reject) => {
|
|
8652
|
+
const child = (0, import_child_process3.spawn)(
|
|
8543
8653
|
"docker",
|
|
8544
|
-
[
|
|
8545
|
-
|
|
8654
|
+
[
|
|
8655
|
+
"build",
|
|
8656
|
+
"--progress=plain",
|
|
8657
|
+
"-t",
|
|
8658
|
+
"factiii-claude",
|
|
8659
|
+
"-f",
|
|
8660
|
+
dockerfilePath,
|
|
8661
|
+
"--label",
|
|
8662
|
+
`${DOCKERFILE_LABEL}=${wantedHash}`,
|
|
8663
|
+
"."
|
|
8664
|
+
],
|
|
8665
|
+
{ env: { ...process.env, DOCKER_BUILDKIT: "1" } }
|
|
8546
8666
|
);
|
|
8547
|
-
|
|
8548
|
-
|
|
8667
|
+
let timer;
|
|
8668
|
+
const arm = () => {
|
|
8669
|
+
clearTimeout(timer);
|
|
8670
|
+
timer = setTimeout(() => {
|
|
8671
|
+
child.kill("SIGKILL");
|
|
8672
|
+
reject(
|
|
8673
|
+
new Error(
|
|
8674
|
+
"Building container image timed out. Check Docker and your connection, then Try Again."
|
|
8675
|
+
)
|
|
8676
|
+
);
|
|
8677
|
+
}, BUILD_TIMEOUT_MS);
|
|
8678
|
+
};
|
|
8679
|
+
arm();
|
|
8680
|
+
const makeLineEmitter = () => {
|
|
8681
|
+
let buffer = "";
|
|
8682
|
+
return (buf) => {
|
|
8683
|
+
arm();
|
|
8684
|
+
buffer += buf.toString();
|
|
8685
|
+
const parts2 = buffer.split("\n");
|
|
8686
|
+
buffer = parts2.pop() ?? "";
|
|
8687
|
+
for (const line of parts2) {
|
|
8688
|
+
const trimmed = line.trimEnd();
|
|
8689
|
+
if (trimmed) onLog?.(trimmed);
|
|
8690
|
+
}
|
|
8691
|
+
};
|
|
8692
|
+
};
|
|
8693
|
+
child.stdout.on("data", makeLineEmitter());
|
|
8694
|
+
child.stderr.on("data", makeLineEmitter());
|
|
8695
|
+
child.on("error", (err) => {
|
|
8696
|
+
clearTimeout(timer);
|
|
8697
|
+
reject(err);
|
|
8698
|
+
});
|
|
8699
|
+
child.on("close", (code) => {
|
|
8700
|
+
clearTimeout(timer);
|
|
8701
|
+
if (code === 0) resolve();
|
|
8702
|
+
else reject(new Error(`docker build failed (exit code ${code}).`));
|
|
8703
|
+
});
|
|
8704
|
+
});
|
|
8705
|
+
}
|
|
8706
|
+
async function ensureCacheVolume() {
|
|
8707
|
+
for (const name of ["factiii-card-cache", "factiii-nix"]) {
|
|
8708
|
+
try {
|
|
8709
|
+
await execFileAsync("docker", ["volume", "inspect", name], {});
|
|
8710
|
+
} catch {
|
|
8711
|
+
await execFileAsync("docker", ["volume", "create", name]);
|
|
8712
|
+
}
|
|
8549
8713
|
}
|
|
8550
8714
|
}
|
|
8551
8715
|
async function killContainer(containerName) {
|
|
@@ -8554,7 +8718,7 @@ async function killContainer(containerName) {
|
|
|
8554
8718
|
} catch {
|
|
8555
8719
|
}
|
|
8556
8720
|
}
|
|
8557
|
-
async function spawnContainer(containerName, claudeToken) {
|
|
8721
|
+
async function spawnContainer(containerName, claudeToken, opts) {
|
|
8558
8722
|
const NUM_PORTS = 6;
|
|
8559
8723
|
const ports = [];
|
|
8560
8724
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8575,10 +8739,22 @@ async function spawnContainer(containerName, claudeToken) {
|
|
|
8575
8739
|
containerName,
|
|
8576
8740
|
"--user",
|
|
8577
8741
|
"claude",
|
|
8742
|
+
// SYS_PTRACE: gdb/strace/valgrind (Docker seccomp gates ptrace on it).
|
|
8743
|
+
// NET_RAW: raw-socket tools (tcpdump/nmap/ping). Scoped to the container.
|
|
8744
|
+
"--cap-add",
|
|
8745
|
+
"SYS_PTRACE",
|
|
8746
|
+
"--cap-add",
|
|
8747
|
+
"NET_RAW",
|
|
8748
|
+
// FUSE for rclone mount on OneDrive spaces (gated - GitHub stays unprivileged).
|
|
8749
|
+
...opts?.fuse ? ["--cap-add", "SYS_ADMIN", "--device", "/dev/fuse"] : [],
|
|
8578
8750
|
"-e",
|
|
8579
8751
|
"HOME=/home/claude",
|
|
8752
|
+
...claudeToken ? ["-e", `CLAUDE_CODE_OAUTH_TOKEN=${claudeToken}`] : [],
|
|
8753
|
+
// Redirect the otherwise-`~/.claude.json` config file into the .claude
|
|
8754
|
+
// dir, which is symlinked to /cache/claude. Keeps all claude state
|
|
8755
|
+
// inside one path the CLI already understands.
|
|
8580
8756
|
"-e",
|
|
8581
|
-
|
|
8757
|
+
"CLAUDE_CONFIG_DIR=/home/claude/.claude",
|
|
8582
8758
|
"-e",
|
|
8583
8759
|
"NODE_OPTIONS=--max-old-space-size=4096",
|
|
8584
8760
|
"-e",
|
|
@@ -8597,6 +8773,8 @@ async function spawnContainer(containerName, claudeToken) {
|
|
|
8597
8773
|
`FACTIII_PORTS=${ports.join(",")}`,
|
|
8598
8774
|
"-v",
|
|
8599
8775
|
"factiii-card-cache:/cache",
|
|
8776
|
+
"-v",
|
|
8777
|
+
"factiii-nix:/nix",
|
|
8600
8778
|
...portArgs,
|
|
8601
8779
|
"factiii-claude",
|
|
8602
8780
|
"tail",
|
|
@@ -8613,6 +8791,37 @@ async function execInContainer(containerName, cmd) {
|
|
|
8613
8791
|
);
|
|
8614
8792
|
return stdout;
|
|
8615
8793
|
}
|
|
8794
|
+
function streamExecInContainer(containerName, cmd, onLine) {
|
|
8795
|
+
const child = (0, import_child_process3.spawn)("docker", [
|
|
8796
|
+
"exec",
|
|
8797
|
+
"--user",
|
|
8798
|
+
"claude",
|
|
8799
|
+
containerName,
|
|
8800
|
+
"sh",
|
|
8801
|
+
"-c",
|
|
8802
|
+
cmd
|
|
8803
|
+
]);
|
|
8804
|
+
let buffer = "";
|
|
8805
|
+
child.stdout.on("data", (buf) => {
|
|
8806
|
+
buffer += buf.toString();
|
|
8807
|
+
const parts2 = buffer.split("\n");
|
|
8808
|
+
buffer = parts2.pop() ?? "";
|
|
8809
|
+
for (const line of parts2) {
|
|
8810
|
+
const trimmed = line.trimEnd();
|
|
8811
|
+
if (trimmed) onLine(trimmed);
|
|
8812
|
+
}
|
|
8813
|
+
});
|
|
8814
|
+
child.on("error", () => {
|
|
8815
|
+
});
|
|
8816
|
+
let stopped = false;
|
|
8817
|
+
return {
|
|
8818
|
+
stop: () => {
|
|
8819
|
+
if (stopped) return;
|
|
8820
|
+
stopped = true;
|
|
8821
|
+
child.kill("SIGKILL");
|
|
8822
|
+
}
|
|
8823
|
+
};
|
|
8824
|
+
}
|
|
8616
8825
|
async function execInContainerAsRoot(containerName, cmd) {
|
|
8617
8826
|
const { stdout } = await execFileAsync(
|
|
8618
8827
|
"docker",
|
|
@@ -8624,15 +8833,19 @@ async function execInContainerAsRoot(containerName, cmd) {
|
|
|
8624
8833
|
async function setupContainerCache(containerName) {
|
|
8625
8834
|
await execInContainer(
|
|
8626
8835
|
containerName,
|
|
8627
|
-
"mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/
|
|
8836
|
+
"mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apt /cache/xdg-cache"
|
|
8628
8837
|
);
|
|
8629
8838
|
await execInContainer(
|
|
8630
8839
|
containerName,
|
|
8631
8840
|
"ln -sfn /cache/claude /home/claude/.claude"
|
|
8632
8841
|
);
|
|
8842
|
+
await execInContainer(
|
|
8843
|
+
containerName,
|
|
8844
|
+
"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"
|
|
8845
|
+
);
|
|
8633
8846
|
await execInContainerAsRoot(
|
|
8634
8847
|
containerName,
|
|
8635
|
-
"ln -sfn /cache/
|
|
8848
|
+
"rm -rf /var/cache/apt/archives && ln -sfn /cache/apt /var/cache/apt/archives && mkdir -p /cache/apt/partial"
|
|
8636
8849
|
);
|
|
8637
8850
|
}
|
|
8638
8851
|
async function isContainerRunning(containerName) {
|
|
@@ -8661,136 +8874,10 @@ async function getContainerPort(containerName, containerPort) {
|
|
|
8661
8874
|
}
|
|
8662
8875
|
}
|
|
8663
8876
|
|
|
8664
|
-
// ../../shared/all/helpers/
|
|
8877
|
+
// ../../shared/all/helpers/board-agent-core/git.ts
|
|
8665
8878
|
var import_child_process4 = require("child_process");
|
|
8666
|
-
var import_fs2 = require("fs");
|
|
8667
|
-
var import_path2 = __toESM(require("path"));
|
|
8668
8879
|
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
8880
|
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
8881
|
function buildCloneUrl(repoUrl, githubToken) {
|
|
8795
8882
|
const url2 = new URL(repoUrl);
|
|
8796
8883
|
url2.username = githubToken;
|
|
@@ -8804,7 +8891,7 @@ function repoCacheKey(repoUrl) {
|
|
|
8804
8891
|
return repoUrl.replace(/^https?:\/\//, "").replace(/\/\/.*@/, "").replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
8805
8892
|
}
|
|
8806
8893
|
async function gitInContainer(containerName, ...args) {
|
|
8807
|
-
const { stdout } = await
|
|
8894
|
+
const { stdout } = await execFileAsync2(
|
|
8808
8895
|
"docker",
|
|
8809
8896
|
[
|
|
8810
8897
|
"exec",
|
|
@@ -8851,7 +8938,7 @@ async function cloneRepo(containerName, cloneUrl, branch, cacheKey) {
|
|
|
8851
8938
|
await gitInContainer(containerName, "clean", "-fd");
|
|
8852
8939
|
return { fromCache: true };
|
|
8853
8940
|
}
|
|
8854
|
-
await
|
|
8941
|
+
await execFileAsync2(
|
|
8855
8942
|
"docker",
|
|
8856
8943
|
[
|
|
8857
8944
|
"exec",
|
|
@@ -8894,6 +8981,7 @@ async function setGitIdentity(containerName, name, email) {
|
|
|
8894
8981
|
);
|
|
8895
8982
|
}
|
|
8896
8983
|
async function extractDiff(containerName) {
|
|
8984
|
+
await gitInContainer(containerName, "add", "--intent-to-add", ".");
|
|
8897
8985
|
const diff = await gitInContainer(containerName, "diff", "HEAD");
|
|
8898
8986
|
const changedFilesRaw = (await gitInContainer(containerName, "diff", "HEAD", "--name-only")).trim();
|
|
8899
8987
|
return {
|
|
@@ -8915,6 +9003,13 @@ async function listRemoteBranches(containerName) {
|
|
|
8915
9003
|
return [];
|
|
8916
9004
|
}
|
|
8917
9005
|
}
|
|
9006
|
+
var NoChangesError = class extends Error {
|
|
9007
|
+
constructor(message = "No changes to commit.") {
|
|
9008
|
+
super(message);
|
|
9009
|
+
this.code = "NO_CHANGES";
|
|
9010
|
+
this.name = "NoChangesError";
|
|
9011
|
+
}
|
|
9012
|
+
};
|
|
8918
9013
|
async function commitAndPush(containerName, branchName, commitMessage, gitConfig) {
|
|
8919
9014
|
if (gitConfig?.gitName)
|
|
8920
9015
|
await gitInContainer(
|
|
@@ -8930,8 +9025,8 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8930
9025
|
"user.email",
|
|
8931
9026
|
gitConfig.gitEmail
|
|
8932
9027
|
);
|
|
8933
|
-
const
|
|
8934
|
-
if (!
|
|
9028
|
+
const status = (await gitInContainer(containerName, "status", "--porcelain")).trim();
|
|
9029
|
+
if (!status) throw new NoChangesError();
|
|
8935
9030
|
let branchExists = false;
|
|
8936
9031
|
try {
|
|
8937
9032
|
await gitInContainer(
|
|
@@ -8946,7 +9041,7 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8946
9041
|
} catch {
|
|
8947
9042
|
}
|
|
8948
9043
|
if (branchExists) {
|
|
8949
|
-
await gitInContainer(containerName, "stash");
|
|
9044
|
+
await gitInContainer(containerName, "stash", "--include-untracked");
|
|
8950
9045
|
await gitInContainer(containerName, "fetch", "origin", branchName);
|
|
8951
9046
|
await gitInContainer(
|
|
8952
9047
|
containerName,
|
|
@@ -8962,128 +9057,273 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8962
9057
|
await gitInContainer(containerName, "add", "-A");
|
|
8963
9058
|
await gitInContainer(containerName, "commit", "-m", commitMessage);
|
|
8964
9059
|
await gitInContainer(containerName, "push", "origin", branchName);
|
|
9060
|
+
await gitInContainer(containerName, "fetch", "origin", branchName);
|
|
9061
|
+
const remoteSha = (await gitInContainer(containerName, "rev-parse", `origin/${branchName}`)).trim();
|
|
9062
|
+
const localSha = (await gitInContainer(containerName, "rev-parse", "HEAD")).trim();
|
|
9063
|
+
if (remoteSha !== localSha) {
|
|
9064
|
+
throw new Error(
|
|
9065
|
+
`Push verification failed: local HEAD ${localSha.slice(0, 8)} does not match origin/${branchName} ${remoteSha.slice(0, 8)}`
|
|
9066
|
+
);
|
|
9067
|
+
}
|
|
9068
|
+
return { pushedSha: localSha, remoteSha, branch: branchName };
|
|
8965
9069
|
}
|
|
8966
9070
|
|
|
8967
|
-
// ../../shared/all/helpers/
|
|
8968
|
-
|
|
9071
|
+
// ../../shared/all/helpers/board-agent-core/onedrive-auth.ts
|
|
9072
|
+
var DEVICECODE_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
|
9073
|
+
var TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
|
9074
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
9075
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
9076
|
+
var ONEDRIVE_SCOPES = "offline_access Files.ReadWrite.AppFolder User.Read";
|
|
9077
|
+
async function startDeviceCode(clientId) {
|
|
9078
|
+
const res = await fetch(DEVICECODE_URL, {
|
|
9079
|
+
method: "POST",
|
|
9080
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9081
|
+
body: new URLSearchParams({ client_id: clientId, scope: ONEDRIVE_SCOPES })
|
|
9082
|
+
});
|
|
9083
|
+
if (!res.ok) {
|
|
9084
|
+
throw new Error(`OneDrive device-code start failed: ${await res.text()}`);
|
|
9085
|
+
}
|
|
9086
|
+
const data = await res.json();
|
|
8969
9087
|
return {
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
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
|
|
9088
|
+
deviceCode: data.device_code,
|
|
9089
|
+
userCode: data.user_code,
|
|
9090
|
+
verificationUri: data.verification_uri,
|
|
9091
|
+
expiresIn: data.expires_in,
|
|
9092
|
+
interval: data.interval
|
|
8985
9093
|
};
|
|
8986
9094
|
}
|
|
8987
|
-
function
|
|
8988
|
-
const
|
|
8989
|
-
|
|
8990
|
-
"
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
9095
|
+
async function refreshAccessToken(clientId, refreshToken) {
|
|
9096
|
+
const res = await fetch(TOKEN_URL, {
|
|
9097
|
+
method: "POST",
|
|
9098
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9099
|
+
body: new URLSearchParams({
|
|
9100
|
+
grant_type: "refresh_token",
|
|
9101
|
+
client_id: clientId,
|
|
9102
|
+
refresh_token: refreshToken,
|
|
9103
|
+
scope: ONEDRIVE_SCOPES
|
|
9104
|
+
})
|
|
9105
|
+
});
|
|
9106
|
+
if (!res.ok) {
|
|
9107
|
+
throw new Error(`OneDrive token refresh failed: ${await res.text()}`);
|
|
9108
|
+
}
|
|
9109
|
+
const tok = await res.json();
|
|
8998
9110
|
return {
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
},
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
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);
|
|
9111
|
+
accessToken: tok.access_token,
|
|
9112
|
+
// Microsoft rotates the refresh token on each refresh; fall back to the old
|
|
9113
|
+
// one if a new one wasn't returned.
|
|
9114
|
+
refreshToken: tok.refresh_token || refreshToken,
|
|
9115
|
+
expiry: new Date(Date.now() + tok.expires_in * 1e3).toISOString()
|
|
9116
|
+
};
|
|
9117
|
+
}
|
|
9118
|
+
async function pollDeviceCode(clientId, deviceCode) {
|
|
9119
|
+
const res = await fetch(TOKEN_URL, {
|
|
9120
|
+
method: "POST",
|
|
9121
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9122
|
+
body: new URLSearchParams({
|
|
9123
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
9124
|
+
client_id: clientId,
|
|
9125
|
+
device_code: deviceCode
|
|
9126
|
+
})
|
|
9127
|
+
});
|
|
9128
|
+
if (res.ok) {
|
|
9129
|
+
const tok = await res.json();
|
|
9130
|
+
if (!tok.refresh_token) {
|
|
9131
|
+
return { status: "error", message: "No refresh token returned." };
|
|
9058
9132
|
}
|
|
9133
|
+
const appFolder = await getAppFolder(tok.access_token);
|
|
9134
|
+
return {
|
|
9135
|
+
status: "complete",
|
|
9136
|
+
refreshToken: tok.refresh_token,
|
|
9137
|
+
appFolder
|
|
9138
|
+
};
|
|
9139
|
+
}
|
|
9140
|
+
const err = await res.json().catch(() => ({}));
|
|
9141
|
+
switch (err.error) {
|
|
9142
|
+
case "authorization_pending":
|
|
9143
|
+
return { status: "pending" };
|
|
9144
|
+
case "slow_down":
|
|
9145
|
+
return { status: "slow_down" };
|
|
9146
|
+
case "expired_token":
|
|
9147
|
+
case "code_expired":
|
|
9148
|
+
return { status: "expired" };
|
|
9149
|
+
case "authorization_declined":
|
|
9150
|
+
case "access_denied":
|
|
9151
|
+
return { status: "declined" };
|
|
9152
|
+
default:
|
|
9153
|
+
return {
|
|
9154
|
+
status: "error",
|
|
9155
|
+
message: err.error_description || err.error || "Device-code poll failed"
|
|
9156
|
+
};
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
async function getAppFolder(accessToken) {
|
|
9160
|
+
const res = await fetch(
|
|
9161
|
+
`${GRAPH_BASE}/me/drive/special/approot?$select=id,parentReference`,
|
|
9162
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
9163
|
+
);
|
|
9164
|
+
if (!res.ok) {
|
|
9165
|
+
throw new Error(
|
|
9166
|
+
`Failed to resolve OneDrive app folder: ${res.status} - ${await res.text()}`
|
|
9167
|
+
);
|
|
9168
|
+
}
|
|
9169
|
+
const data = await res.json();
|
|
9170
|
+
if (!data.id || !data.parentReference?.driveId) {
|
|
9171
|
+
throw new Error("OneDrive app folder response was incomplete.");
|
|
9172
|
+
}
|
|
9173
|
+
return {
|
|
9174
|
+
rootFolderId: data.id,
|
|
9175
|
+
driveId: data.parentReference.driveId,
|
|
9176
|
+
driveType: data.parentReference.driveType || "personal"
|
|
9059
9177
|
};
|
|
9060
9178
|
}
|
|
9061
9179
|
|
|
9062
|
-
// ../../shared/all/helpers/
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9180
|
+
// ../../shared/all/helpers/board-agent-core/onedrive.ts
|
|
9181
|
+
var WORKSPACE = "/home/claude/workspace";
|
|
9182
|
+
var RCLONE_CONF = "/home/claude/.config/rclone/rclone.conf";
|
|
9183
|
+
var REMOTE = "onedrive";
|
|
9184
|
+
var SYNC_LOG = "/tmp/rclone-onedrive.log";
|
|
9185
|
+
function onedriveRemotePath(spaceSlug) {
|
|
9186
|
+
const safe = spaceSlug.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
9187
|
+
return `${REMOTE}:${safe}`;
|
|
9188
|
+
}
|
|
9189
|
+
async function writeRcloneMountConfig(containerName, conn) {
|
|
9190
|
+
const fresh = await refreshAccessToken(conn.clientId, conn.refreshToken);
|
|
9191
|
+
const tokenJson = JSON.stringify({
|
|
9192
|
+
access_token: fresh.accessToken,
|
|
9193
|
+
refresh_token: fresh.refreshToken,
|
|
9194
|
+
token_type: "Bearer",
|
|
9195
|
+
expiry: fresh.expiry
|
|
9196
|
+
});
|
|
9197
|
+
const conf = [
|
|
9198
|
+
`[${REMOTE}]`,
|
|
9199
|
+
"type = onedrive",
|
|
9200
|
+
`client_id = ${conn.clientId}`,
|
|
9201
|
+
`drive_id = ${conn.driveId}`,
|
|
9202
|
+
`drive_type = ${conn.driveType}`,
|
|
9203
|
+
`root_folder_id = ${conn.rootFolderId}`,
|
|
9204
|
+
// App is consumer-only, so rclone must refresh against /consumers (not the
|
|
9205
|
+
// default /common, which rejects it with AADSTS9002346).
|
|
9206
|
+
"auth_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
|
|
9207
|
+
"token_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
|
9208
|
+
`token = ${tokenJson}`,
|
|
9209
|
+
""
|
|
9210
|
+
].join("\n");
|
|
9211
|
+
const b64 = Buffer.from(conf, "utf-8").toString("base64");
|
|
9212
|
+
await execInContainer(
|
|
9213
|
+
containerName,
|
|
9214
|
+
`mkdir -p /home/claude/.config/rclone && printf %s '${b64}' | base64 -d > ${RCLONE_CONF} && chmod 600 ${RCLONE_CONF}`
|
|
9215
|
+
);
|
|
9216
|
+
return fresh.refreshToken;
|
|
9217
|
+
}
|
|
9218
|
+
async function mountWorkspace(containerName, remotePath) {
|
|
9219
|
+
await execInContainer(
|
|
9220
|
+
containerName,
|
|
9221
|
+
`mkdir -p ${WORKSPACE} && rclone mkdir '${remotePath}'`
|
|
9222
|
+
);
|
|
9223
|
+
const out = await execInContainer(
|
|
9224
|
+
containerName,
|
|
9225
|
+
`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)`
|
|
9226
|
+
);
|
|
9227
|
+
if (!out.trim().endsWith("mounted")) {
|
|
9228
|
+
throw new Error(`OneDrive mount failed:
|
|
9229
|
+
${out.trim()}`);
|
|
9067
9230
|
}
|
|
9068
|
-
return text;
|
|
9069
9231
|
}
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9232
|
+
var syncWatchers = /* @__PURE__ */ new Map();
|
|
9233
|
+
var RC_ADDR = "127.0.0.1:5572";
|
|
9234
|
+
async function onedriveQueueNow(containerName) {
|
|
9235
|
+
const out = await execInContainer(
|
|
9236
|
+
containerName,
|
|
9237
|
+
`rclone rc --url http://${RC_ADDR}/ vfs/queue 2>/dev/null || echo '{}'`
|
|
9238
|
+
);
|
|
9239
|
+
try {
|
|
9240
|
+
const parsed = JSON.parse(out);
|
|
9241
|
+
return (parsed.queue ?? []).map((i) => i.name).filter((n) => !!n);
|
|
9242
|
+
} catch {
|
|
9243
|
+
return [];
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
function startOneDriveSyncWatcher(containerName, onPending) {
|
|
9247
|
+
stopOneDriveSyncWatcher(containerName);
|
|
9248
|
+
let chain = Promise.resolve();
|
|
9249
|
+
const refresh = () => {
|
|
9250
|
+
chain = chain.then(async () => {
|
|
9251
|
+
const watcher = syncWatchers.get(containerName);
|
|
9252
|
+
if (!watcher) return;
|
|
9253
|
+
const files = await onedriveQueueNow(containerName);
|
|
9254
|
+
if (watcher.files.join("\n") !== files.join("\n")) {
|
|
9255
|
+
watcher.files = files;
|
|
9256
|
+
onPending(files.length, files);
|
|
9257
|
+
}
|
|
9258
|
+
});
|
|
9259
|
+
};
|
|
9260
|
+
const handle = streamExecInContainer(
|
|
9261
|
+
containerName,
|
|
9262
|
+
`tail -n0 -F ${SYNC_LOG} 2>/dev/null | grep --line-buffered -E 'vfs cache: (upload|starting|failed|cancel|queuing)'`,
|
|
9263
|
+
refresh
|
|
9264
|
+
);
|
|
9265
|
+
syncWatchers.set(containerName, { handle, files: [] });
|
|
9266
|
+
}
|
|
9267
|
+
function stopOneDriveSyncWatcher(containerName) {
|
|
9268
|
+
const watcher = syncWatchers.get(containerName);
|
|
9269
|
+
if (watcher) {
|
|
9270
|
+
watcher.handle.stop();
|
|
9271
|
+
syncWatchers.delete(containerName);
|
|
9272
|
+
}
|
|
9074
9273
|
}
|
|
9075
|
-
|
|
9274
|
+
function onedrivePendingUploads(containerName) {
|
|
9275
|
+
return syncWatchers.get(containerName)?.files.length ?? 0;
|
|
9276
|
+
}
|
|
9277
|
+
function onedriveUploadingFiles(containerName) {
|
|
9278
|
+
return syncWatchers.get(containerName)?.files ?? [];
|
|
9279
|
+
}
|
|
9280
|
+
async function unmountWorkspace(containerName) {
|
|
9281
|
+
await execInContainer(
|
|
9282
|
+
containerName,
|
|
9283
|
+
`fusermount -u ${WORKSPACE} 2>/dev/null || fusermount3 -u ${WORKSPACE} 2>/dev/null || rclone unmount ${WORKSPACE} 2>/dev/null || true`
|
|
9284
|
+
);
|
|
9285
|
+
}
|
|
9286
|
+
async function readbackRefreshToken(containerName) {
|
|
9287
|
+
let conf = "";
|
|
9288
|
+
try {
|
|
9289
|
+
conf = await execInContainer(
|
|
9290
|
+
containerName,
|
|
9291
|
+
`cat ${RCLONE_CONF} 2>/dev/null || true`
|
|
9292
|
+
);
|
|
9293
|
+
} catch {
|
|
9294
|
+
return null;
|
|
9295
|
+
}
|
|
9296
|
+
const match = conf.match(/token\s*=\s*(\{.*\})/);
|
|
9297
|
+
if (!match) return null;
|
|
9298
|
+
try {
|
|
9299
|
+
const parsed = JSON.parse(match[1]);
|
|
9300
|
+
return parsed.refresh_token ?? null;
|
|
9301
|
+
} catch {
|
|
9302
|
+
return null;
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
|
|
9306
|
+
// ../../shared/all/helpers/board-agent-core/container.ts
|
|
9307
|
+
function withTimeout(work, ms, label) {
|
|
9308
|
+
let timer;
|
|
9309
|
+
const timeout = new Promise((_, reject) => {
|
|
9310
|
+
timer = setTimeout(
|
|
9311
|
+
() => reject(
|
|
9312
|
+
new Error(
|
|
9313
|
+
`${label} timed out. Check Docker and your connection, then Try Again.`
|
|
9314
|
+
)
|
|
9315
|
+
),
|
|
9316
|
+
ms
|
|
9317
|
+
);
|
|
9318
|
+
});
|
|
9319
|
+
return Promise.race([work.finally(() => clearTimeout(timer)), timeout]);
|
|
9320
|
+
}
|
|
9321
|
+
var ContainerCore = class {
|
|
9076
9322
|
constructor(options) {
|
|
9077
|
-
// Utility container state
|
|
9323
|
+
// Utility container state.
|
|
9078
9324
|
this.containerName = null;
|
|
9079
9325
|
this.lastConfigKey = "";
|
|
9080
9326
|
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
9327
|
this.spaceSlug = options.spaceSlug;
|
|
9088
9328
|
this.emitter = options.emitter;
|
|
9089
9329
|
this.configProvider = options.configProvider;
|
|
@@ -9092,6 +9332,49 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9092
9332
|
const raw = (options.instanceId || "shared").toLowerCase();
|
|
9093
9333
|
this.instancePrefix = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
|
|
9094
9334
|
}
|
|
9335
|
+
readConfig() {
|
|
9336
|
+
return this.configProvider.readConfig(this.spaceSlug);
|
|
9337
|
+
}
|
|
9338
|
+
isReady() {
|
|
9339
|
+
return this.containerName !== null;
|
|
9340
|
+
}
|
|
9341
|
+
// ── Storage backend ──
|
|
9342
|
+
isOneDrive(config) {
|
|
9343
|
+
return config.storageBackend === "onedrive";
|
|
9344
|
+
}
|
|
9345
|
+
/** Per-space OneDrive remote path inside the app folder. */
|
|
9346
|
+
remotePath() {
|
|
9347
|
+
return onedriveRemotePath(this.spaceSlug);
|
|
9348
|
+
}
|
|
9349
|
+
isOneDriveConnected() {
|
|
9350
|
+
return this.configProvider.readOneDriveConnection(this.spaceSlug) !== null;
|
|
9351
|
+
}
|
|
9352
|
+
/** The stored OneDrive credential, or fail with a connect hint. */
|
|
9353
|
+
getOneDriveConnection() {
|
|
9354
|
+
const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
|
|
9355
|
+
if (!conn) {
|
|
9356
|
+
throw new Error(
|
|
9357
|
+
"OneDrive is not connected. Connect it in Board AI settings."
|
|
9358
|
+
);
|
|
9359
|
+
}
|
|
9360
|
+
return conn;
|
|
9361
|
+
}
|
|
9362
|
+
saveOneDriveConnection(connection) {
|
|
9363
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, connection);
|
|
9364
|
+
}
|
|
9365
|
+
clearOneDriveConnection() {
|
|
9366
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, null);
|
|
9367
|
+
}
|
|
9368
|
+
/** Persist a rotated refresh token onto the existing connection. */
|
|
9369
|
+
updateOneDriveRefreshToken(refreshToken) {
|
|
9370
|
+
const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
|
|
9371
|
+
if (conn) {
|
|
9372
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, {
|
|
9373
|
+
...conn,
|
|
9374
|
+
refreshToken
|
|
9375
|
+
});
|
|
9376
|
+
}
|
|
9377
|
+
}
|
|
9095
9378
|
// ── Config ──
|
|
9096
9379
|
getConfig() {
|
|
9097
9380
|
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
@@ -9103,7 +9386,11 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9103
9386
|
hasToken: Boolean(config.claudeToken),
|
|
9104
9387
|
hasGithubToken: Boolean(config.githubToken),
|
|
9105
9388
|
tokenHint: config.claudeToken ? `\xB7\xB7\xB7\xB7${config.claudeToken.slice(-4)}` : "",
|
|
9106
|
-
githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : ""
|
|
9389
|
+
githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : "",
|
|
9390
|
+
storageBackend: config.storageBackend ?? "github",
|
|
9391
|
+
onedrive: {
|
|
9392
|
+
connected: this.isOneDriveConnected()
|
|
9393
|
+
}
|
|
9107
9394
|
};
|
|
9108
9395
|
}
|
|
9109
9396
|
setConfig(partial) {
|
|
@@ -9114,7 +9401,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9114
9401
|
gitName: partial.gitName ?? existing.gitName,
|
|
9115
9402
|
gitEmail: partial.gitEmail ?? existing.gitEmail,
|
|
9116
9403
|
claudeToken: partial.claudeToken?.replace(/\s/g, "") || existing.claudeToken,
|
|
9117
|
-
githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken
|
|
9404
|
+
githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken,
|
|
9405
|
+
storageBackend: partial.storageBackend ?? existing.storageBackend
|
|
9118
9406
|
};
|
|
9119
9407
|
this.configProvider.writeConfig(this.spaceSlug, updated);
|
|
9120
9408
|
return { success: true };
|
|
@@ -9129,10 +9417,19 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9129
9417
|
async ensureContainer(onStatus) {
|
|
9130
9418
|
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
9131
9419
|
if (!config.claudeToken) throw new Error("No Claude token configured.");
|
|
9132
|
-
|
|
9133
|
-
if (
|
|
9134
|
-
|
|
9135
|
-
|
|
9420
|
+
const onedrive = this.isOneDrive(config);
|
|
9421
|
+
if (onedrive) {
|
|
9422
|
+
if (!this.isOneDriveConnected()) {
|
|
9423
|
+
throw new Error(
|
|
9424
|
+
"OneDrive is not connected. Connect it in Board AI settings, then retry."
|
|
9425
|
+
);
|
|
9426
|
+
}
|
|
9427
|
+
} else {
|
|
9428
|
+
if (!config.githubToken) throw new Error("No GitHub token configured.");
|
|
9429
|
+
if (!config.repoUrl) throw new Error("No repository URL configured.");
|
|
9430
|
+
}
|
|
9431
|
+
const configKey = onedrive ? `onedrive|${this.spaceSlug}|${config.claudeToken}` : `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
|
|
9432
|
+
const cacheKey = onedrive ? `onedrive-${this.spaceSlug}`.replace(/[^a-zA-Z0-9._-]/g, "_") : repoCacheKey(config.repoUrl);
|
|
9136
9433
|
const name = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
|
|
9137
9434
|
if (this.containerName && this.lastConfigKey !== configKey) {
|
|
9138
9435
|
await killContainer(this.containerName);
|
|
@@ -9154,16 +9451,40 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9154
9451
|
this.emitter.emitSetupStatus(msg);
|
|
9155
9452
|
};
|
|
9156
9453
|
try {
|
|
9157
|
-
status("
|
|
9158
|
-
await
|
|
9454
|
+
status("Step 1/4 \xB7 Checking Docker\u2026");
|
|
9455
|
+
await withTimeout(assertDockerRunning(), 15e3, "Checking Docker");
|
|
9456
|
+
status("Step 2/4 \xB7 Building container image\u2026");
|
|
9457
|
+
await ensureDockerImage(this.dockerfilePath, (line) => status(line));
|
|
9159
9458
|
await ensureCacheVolume();
|
|
9160
9459
|
await killContainer(name);
|
|
9161
|
-
status("Starting container
|
|
9162
|
-
await
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9460
|
+
status("Step 3/4 \xB7 Starting container\u2026");
|
|
9461
|
+
await withTimeout(
|
|
9462
|
+
spawnContainer(name, config.claudeToken),
|
|
9463
|
+
9e4,
|
|
9464
|
+
"Starting container"
|
|
9465
|
+
);
|
|
9466
|
+
if (onedrive) {
|
|
9467
|
+
status("Step 4/4 \xB7 Verifying OneDrive\u2026");
|
|
9468
|
+
const rotated = await writeRcloneMountConfig(
|
|
9469
|
+
name,
|
|
9470
|
+
this.getOneDriveConnection()
|
|
9471
|
+
);
|
|
9472
|
+
this.updateOneDriveRefreshToken(rotated);
|
|
9473
|
+
await withTimeout(
|
|
9474
|
+
execInContainer(name, `rclone lsd 'onedrive:'`),
|
|
9475
|
+
6e4,
|
|
9476
|
+
"Verifying OneDrive"
|
|
9477
|
+
);
|
|
9478
|
+
} else {
|
|
9479
|
+
status("Step 4/4 \xB7 Cloning repository\u2026");
|
|
9480
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
9481
|
+
await withTimeout(
|
|
9482
|
+
cloneRepo(name, cloneUrl, config.mainBranch || "main", cacheKey),
|
|
9483
|
+
18e4,
|
|
9484
|
+
"Cloning repository"
|
|
9485
|
+
);
|
|
9486
|
+
await setGitIdentity(name, config.gitName, config.gitEmail);
|
|
9487
|
+
}
|
|
9167
9488
|
status("");
|
|
9168
9489
|
this.containerName = name;
|
|
9169
9490
|
this.lastConfigKey = configKey;
|
|
@@ -9175,200 +9496,1534 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9175
9496
|
})();
|
|
9176
9497
|
return this.setupPromise;
|
|
9177
9498
|
}
|
|
9178
|
-
|
|
9179
|
-
|
|
9499
|
+
setupStatus() {
|
|
9500
|
+
if (this.containerName) return { phase: "ready" };
|
|
9501
|
+
if (this.setupPromise) return { phase: "running" };
|
|
9502
|
+
return { phase: "idle" };
|
|
9503
|
+
}
|
|
9504
|
+
async ensureSetup() {
|
|
9505
|
+
await this.ensureContainer();
|
|
9506
|
+
return { ready: true };
|
|
9507
|
+
}
|
|
9508
|
+
async cardRemoteBranches() {
|
|
9509
|
+
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
9510
|
+
if (this.isOneDrive(config)) return [];
|
|
9511
|
+
if (!config.githubToken || !config.repoUrl) return [];
|
|
9180
9512
|
if (!this.isReady()) {
|
|
9181
9513
|
throw new Error(
|
|
9182
9514
|
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9183
9515
|
);
|
|
9184
9516
|
}
|
|
9185
|
-
|
|
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;
|
|
9517
|
+
return listRemoteBranches(this.containerName);
|
|
9225
9518
|
}
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
containerName
|
|
9230
|
-
|
|
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);
|
|
9519
|
+
destroy() {
|
|
9520
|
+
if (this.containerName) {
|
|
9521
|
+
void killContainer(this.containerName);
|
|
9522
|
+
this.containerName = null;
|
|
9523
|
+
this.setupPromise = null;
|
|
9257
9524
|
}
|
|
9258
9525
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9526
|
+
};
|
|
9527
|
+
|
|
9528
|
+
// ../../shared/all/helpers/board-agent-core/runtimes.ts
|
|
9529
|
+
async function installRuntimes(emitter, session, attrs) {
|
|
9530
|
+
const clean = attrs.map((a) => a.trim()).filter((a) => a && /^[a-zA-Z0-9._-]+$/.test(a));
|
|
9531
|
+
if (!clean.length) return;
|
|
9532
|
+
emitter.addLog(
|
|
9533
|
+
session,
|
|
9534
|
+
"system",
|
|
9535
|
+
`Installing ${clean.join(", ")} via Nix...`
|
|
9536
|
+
);
|
|
9537
|
+
try {
|
|
9538
|
+
const refs = clean.map((a) => `nixpkgs#${a}`).join(" ");
|
|
9539
|
+
const wrapped = `nix profile install ${refs} 2>&1; echo "__FACTIII_NIX_EXIT__:$?"`;
|
|
9540
|
+
const out = await execInContainer(session.containerName, wrapped);
|
|
9541
|
+
const marker = out.lastIndexOf("__FACTIII_NIX_EXIT__:");
|
|
9542
|
+
const body = (marker >= 0 ? out.slice(0, marker) : out).trimEnd();
|
|
9543
|
+
const code = marker >= 0 ? parseInt(out.slice(marker).split(":")[1], 10) || 0 : 0;
|
|
9544
|
+
if (body) emitter.addLog(session, "text", body);
|
|
9545
|
+
emitter.addLog(
|
|
9266
9546
|
session,
|
|
9267
|
-
|
|
9268
|
-
"
|
|
9269
|
-
resumeSessionId,
|
|
9270
|
-
{ permissionMode: "plan", disallowedTools: "AskUserQuestion" }
|
|
9547
|
+
code === 0 ? "system" : "error",
|
|
9548
|
+
code === 0 ? "Environment ready." : `Install exited with code ${code}. Continuing \u2014 install by hand from the terminal if needed.`
|
|
9271
9549
|
);
|
|
9272
|
-
|
|
9273
|
-
|
|
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(
|
|
9550
|
+
} catch (err) {
|
|
9551
|
+
emitter.addLog(
|
|
9280
9552
|
session,
|
|
9281
|
-
"
|
|
9282
|
-
|
|
9553
|
+
"error",
|
|
9554
|
+
`Install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
9283
9555
|
);
|
|
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
9556
|
}
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9557
|
+
}
|
|
9558
|
+
|
|
9559
|
+
// ../../shared/all/helpers/board-agent-core/session.ts
|
|
9560
|
+
function toSummary(session) {
|
|
9561
|
+
return {
|
|
9562
|
+
taskId: session.taskId,
|
|
9563
|
+
postId: session.postId,
|
|
9564
|
+
phase: session.phase,
|
|
9565
|
+
mode: session.mode,
|
|
9566
|
+
step: session.step,
|
|
9567
|
+
diff: session.diff,
|
|
9568
|
+
changedFiles: session.changedFiles,
|
|
9569
|
+
commitMessage: session.commitMessage,
|
|
9570
|
+
previewUrl: session.previewUrl,
|
|
9571
|
+
previewRunning: session.previewRunning,
|
|
9572
|
+
reproSteps: session.reproSteps,
|
|
9573
|
+
pendingQuestion: session.pendingQuestion,
|
|
9574
|
+
error: session.error,
|
|
9575
|
+
startedAt: session.startedAt,
|
|
9576
|
+
targetBranch: session.targetBranch,
|
|
9577
|
+
taskTitle: session.taskTitle,
|
|
9578
|
+
todos: session.todos
|
|
9579
|
+
};
|
|
9580
|
+
}
|
|
9581
|
+
function createEventSink(send, spaceSlug, notify) {
|
|
9582
|
+
const PHASE_LABELS = {
|
|
9583
|
+
research: "Research started",
|
|
9584
|
+
"awaiting-input": "Input required",
|
|
9585
|
+
implement: "Implementation started",
|
|
9586
|
+
review: "Review started",
|
|
9587
|
+
complete: "Task complete",
|
|
9588
|
+
error: "Task failed"
|
|
9589
|
+
};
|
|
9590
|
+
const channel = (event) => `board-agent:${spaceSlug}:${event}`;
|
|
9591
|
+
return {
|
|
9592
|
+
emitSessionUpdate(session) {
|
|
9593
|
+
send(channel("session-update"), toSummary(session));
|
|
9594
|
+
const label = PHASE_LABELS[session.phase];
|
|
9595
|
+
if (label && notify) {
|
|
9596
|
+
notify(
|
|
9597
|
+
`Board AI - ${label}`,
|
|
9598
|
+
`${session.taskTitle || "Card"} (step ${session.step}/3)`,
|
|
9599
|
+
session.phase !== "complete" && session.phase !== "error" && session.phase !== "awaiting-input"
|
|
9600
|
+
);
|
|
9601
|
+
}
|
|
9602
|
+
},
|
|
9603
|
+
emitSessionRemoved(postId) {
|
|
9604
|
+
send(channel("session-update"), {
|
|
9605
|
+
postId,
|
|
9606
|
+
phase: null,
|
|
9607
|
+
mode: "claude",
|
|
9608
|
+
step: 1,
|
|
9609
|
+
taskId: "",
|
|
9610
|
+
diff: "",
|
|
9611
|
+
changedFiles: [],
|
|
9612
|
+
commitMessage: "",
|
|
9613
|
+
previewUrl: "",
|
|
9614
|
+
previewRunning: false,
|
|
9615
|
+
reproSteps: "",
|
|
9616
|
+
pendingQuestion: "",
|
|
9617
|
+
error: "",
|
|
9618
|
+
startedAt: 0,
|
|
9619
|
+
targetBranch: "",
|
|
9620
|
+
taskTitle: "",
|
|
9621
|
+
todos: []
|
|
9622
|
+
});
|
|
9623
|
+
},
|
|
9624
|
+
emitLog(session, entry) {
|
|
9625
|
+
send(channel(`log:${session.postId}`), entry);
|
|
9626
|
+
},
|
|
9627
|
+
addLog(session, type, content) {
|
|
9628
|
+
const entry = { time: Date.now(), type, content };
|
|
9629
|
+
session.logs.push(entry);
|
|
9630
|
+
this.emitLog(session, entry);
|
|
9631
|
+
},
|
|
9632
|
+
appendTextLog(session, text) {
|
|
9633
|
+
const last = session.logs[session.logs.length - 1];
|
|
9634
|
+
if (last && last.type === "text") {
|
|
9635
|
+
last.content += text;
|
|
9636
|
+
this.emitLog(session, {
|
|
9637
|
+
time: last.time,
|
|
9638
|
+
type: "text-append",
|
|
9639
|
+
content: text
|
|
9640
|
+
});
|
|
9641
|
+
} else {
|
|
9642
|
+
const entry = {
|
|
9643
|
+
time: Date.now(),
|
|
9644
|
+
type: "text",
|
|
9645
|
+
content: text
|
|
9646
|
+
};
|
|
9647
|
+
session.logs.push(entry);
|
|
9648
|
+
this.emitLog(session, entry);
|
|
9649
|
+
}
|
|
9650
|
+
},
|
|
9651
|
+
emitSetupStatus(message) {
|
|
9652
|
+
send(channel("setup-status"), message);
|
|
9653
|
+
},
|
|
9654
|
+
emitOneDriveAuth(payload) {
|
|
9655
|
+
send(channel("onedrive-auth"), payload);
|
|
9656
|
+
},
|
|
9657
|
+
emitOneDriveSync(payload) {
|
|
9658
|
+
send(channel("onedrive-sync"), payload);
|
|
9659
|
+
}
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
|
|
9663
|
+
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9664
|
+
var import_child_process5 = require("child_process");
|
|
9665
|
+
var import_fs2 = require("fs");
|
|
9666
|
+
var import_path2 = __toESM(require("path"));
|
|
9667
|
+
var import_util3 = require("util");
|
|
9668
|
+
|
|
9669
|
+
// ../../shared/all/helpers/card-ai/scripts.ts
|
|
9670
|
+
var FRPC_EXPOSE_SCRIPT = `#!/bin/sh
|
|
9671
|
+
set -e
|
|
9672
|
+
set -a; . /etc/environment.factiii 2>/dev/null || true; set +a
|
|
9673
|
+
PORT="$1"
|
|
9674
|
+
NAME="$2"
|
|
9675
|
+
if [ -z "$PORT" ] || [ -z "$NAME" ]; then
|
|
9676
|
+
echo "usage: factiii-expose <port> <name>" >&2
|
|
9677
|
+
echo " e.g. factiii-expose 3000 app" >&2
|
|
9678
|
+
exit 1
|
|
9679
|
+
fi
|
|
9680
|
+
case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
|
|
9681
|
+
PROXY="\${FACTIII_TASK_ID}-\${NAME}"
|
|
9682
|
+
DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
|
|
9683
|
+
TOML="/tmp/frpc-\${NAME}.toml"
|
|
9684
|
+
cat > "$TOML" <<EOF
|
|
9685
|
+
serverAddr = "\${FACTIII_FRP_SERVER}"
|
|
9686
|
+
serverPort = \${FACTIII_FRP_PORT}
|
|
9687
|
+
transport.protocol = "tcp"
|
|
9688
|
+
|
|
9689
|
+
[metadatas]
|
|
9690
|
+
factiii_token = "\${FACTIII_FRP_TOKEN}"
|
|
9691
|
+
|
|
9692
|
+
[[proxies]]
|
|
9693
|
+
name = "\${PROXY}"
|
|
9694
|
+
type = "http"
|
|
9695
|
+
localPort = \${PORT}
|
|
9696
|
+
subdomain = "\${PROXY}"
|
|
9697
|
+
EOF
|
|
9698
|
+
setsid frpc -c "$TOML" >/tmp/frpc-\${NAME}.log 2>&1 < /dev/null &
|
|
9699
|
+
echo $! > /tmp/frpc-\${NAME}.pid
|
|
9700
|
+
echo "http://\${DOMAIN}"
|
|
9701
|
+
`;
|
|
9702
|
+
var FRPC_UNEXPOSE_SCRIPT = `#!/bin/sh
|
|
9703
|
+
NAME="$1"
|
|
9704
|
+
if [ -z "$NAME" ]; then
|
|
9705
|
+
echo "usage: factiii-unexpose <name>" >&2
|
|
9706
|
+
exit 1
|
|
9707
|
+
fi
|
|
9708
|
+
PID_FILE="/tmp/frpc-\${NAME}.pid"
|
|
9709
|
+
if [ -f "$PID_FILE" ]; then
|
|
9710
|
+
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
|
9711
|
+
rm -f "$PID_FILE" "/tmp/frpc-\${NAME}.toml" "/tmp/frpc-\${NAME}.log"
|
|
9712
|
+
echo "stopped \${NAME}"
|
|
9713
|
+
else
|
|
9714
|
+
echo "no tunnel named \${NAME}" >&2
|
|
9715
|
+
exit 1
|
|
9716
|
+
fi
|
|
9717
|
+
`;
|
|
9718
|
+
|
|
9719
|
+
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9720
|
+
var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
|
|
9721
|
+
var FRP_SERVER_ADDR = "dev.factiii.com";
|
|
9722
|
+
var FRP_SERVER_PORT = 7443;
|
|
9723
|
+
var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
|
|
9724
|
+
async function writeFileAsRoot(containerName, path7, contents) {
|
|
9725
|
+
const b64 = Buffer.from(contents, "utf-8").toString("base64");
|
|
9726
|
+
await execInContainerAsRoot(
|
|
9727
|
+
containerName,
|
|
9728
|
+
`echo '${b64}' | base64 -d > '${path7}'`
|
|
9729
|
+
);
|
|
9730
|
+
}
|
|
9731
|
+
async function installFrpcBinary(containerName) {
|
|
9732
|
+
const arch = (await execInContainer(containerName, "uname -m")).trim();
|
|
9733
|
+
const map = {
|
|
9734
|
+
x86_64: "frpc-linux-amd64",
|
|
9735
|
+
aarch64: "frpc-linux-arm64"
|
|
9736
|
+
};
|
|
9737
|
+
const binaryName = map[arch];
|
|
9738
|
+
if (!binaryName) {
|
|
9739
|
+
throw new Error(`Unsupported container arch for frpc: ${arch}`);
|
|
9740
|
+
}
|
|
9741
|
+
const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
|
|
9742
|
+
if (!(0, import_fs2.existsSync)(src)) {
|
|
9743
|
+
throw new Error(
|
|
9744
|
+
`Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
|
|
9745
|
+
);
|
|
9746
|
+
}
|
|
9747
|
+
await execFileAsync3("docker", [
|
|
9748
|
+
"cp",
|
|
9749
|
+
src,
|
|
9750
|
+
`${containerName}:/usr/local/bin/frpc`
|
|
9751
|
+
]);
|
|
9752
|
+
await execInContainerAsRoot(containerName, "chmod +x /usr/local/bin/frpc");
|
|
9753
|
+
}
|
|
9754
|
+
async function installFrpcHarness(params) {
|
|
9755
|
+
const { containerName, taskId, runnerToken } = params;
|
|
9756
|
+
await installFrpcBinary(containerName);
|
|
9757
|
+
const envLines = [
|
|
9758
|
+
`FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
|
|
9759
|
+
`FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
|
|
9760
|
+
`FACTIII_FRP_TOKEN=${runnerToken}`,
|
|
9761
|
+
`FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
|
|
9762
|
+
`FACTIII_TASK_ID=${taskId}`
|
|
9763
|
+
];
|
|
9764
|
+
await writeFileAsRoot(
|
|
9765
|
+
containerName,
|
|
9766
|
+
"/usr/local/bin/factiii-expose",
|
|
9767
|
+
FRPC_EXPOSE_SCRIPT
|
|
9768
|
+
);
|
|
9769
|
+
await writeFileAsRoot(
|
|
9770
|
+
containerName,
|
|
9771
|
+
"/usr/local/bin/factiii-unexpose",
|
|
9772
|
+
FRPC_UNEXPOSE_SCRIPT
|
|
9773
|
+
);
|
|
9774
|
+
await execInContainerAsRoot(
|
|
9775
|
+
containerName,
|
|
9776
|
+
"chmod +x /usr/local/bin/factiii-expose /usr/local/bin/factiii-unexpose"
|
|
9777
|
+
);
|
|
9778
|
+
await writeFileAsRoot(
|
|
9779
|
+
containerName,
|
|
9780
|
+
"/etc/environment.factiii",
|
|
9781
|
+
envLines.join("\n") + "\n"
|
|
9782
|
+
);
|
|
9783
|
+
await execInContainerAsRoot(
|
|
9784
|
+
containerName,
|
|
9785
|
+
`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`
|
|
9786
|
+
);
|
|
9787
|
+
}
|
|
9788
|
+
|
|
9789
|
+
// ../../shared/all/helpers/card-ai/claude-mode.ts
|
|
9790
|
+
function buildPrompt(lines, vars = {}) {
|
|
9791
|
+
let text = lines.join("\n");
|
|
9792
|
+
for (const [key, value2] of Object.entries(vars)) {
|
|
9793
|
+
text = text.replaceAll(`{{${key}}}`, value2);
|
|
9794
|
+
}
|
|
9795
|
+
return text;
|
|
9796
|
+
}
|
|
9797
|
+
var ONEDRIVE_PROMPT_SWAPS = [
|
|
9798
|
+
[
|
|
9799
|
+
"- Full clone with complete git history available.",
|
|
9800
|
+
"- Your file changes are saved automatically when the task finishes."
|
|
9801
|
+
],
|
|
9802
|
+
[
|
|
9803
|
+
"1. Review the diff of your changes (git diff HEAD)",
|
|
9804
|
+
"1. Review the changes you made"
|
|
9805
|
+
]
|
|
9806
|
+
];
|
|
9807
|
+
function gitFree(text) {
|
|
9808
|
+
let out = text;
|
|
9809
|
+
for (const [from, to] of ONEDRIVE_PROMPT_SWAPS) out = out.replace(from, to);
|
|
9810
|
+
return out;
|
|
9811
|
+
}
|
|
9812
|
+
var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
9813
|
+
constructor(core, emitter) {
|
|
9814
|
+
this.core = core;
|
|
9815
|
+
this.emitter = emitter;
|
|
9816
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
9817
|
+
this.board = { sessionId: null, process: null, messages: [] };
|
|
9818
|
+
}
|
|
9819
|
+
static {
|
|
9820
|
+
this.QUESTION_MARKER = "AWAITING_INPUT";
|
|
9821
|
+
}
|
|
9822
|
+
static {
|
|
9823
|
+
this.FULL_TOOLS = "Read,Grep,Glob,Agent,Edit,Write,Bash,TodoWrite";
|
|
9824
|
+
}
|
|
9825
|
+
// ── Board AI ("Ask AI") ──
|
|
9826
|
+
async ask(params, sendLog) {
|
|
9827
|
+
if (!this.core.isReady()) {
|
|
9828
|
+
throw new Error(
|
|
9829
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9830
|
+
);
|
|
9831
|
+
}
|
|
9832
|
+
const containerName = this.core.containerName;
|
|
9833
|
+
await execInContainer(containerName, "mkdir -p /home/claude/workspace");
|
|
9834
|
+
this.board.messages.push({ role: "user", content: params.question });
|
|
9835
|
+
let prompt2;
|
|
9836
|
+
if (this.board.sessionId) {
|
|
9837
|
+
prompt2 = params.question;
|
|
9838
|
+
} else {
|
|
9839
|
+
const parts2 = ["Here is the board data:", "", params.boardContext || ""];
|
|
9840
|
+
parts2.push("", params.question);
|
|
9841
|
+
prompt2 = parts2.join("\n");
|
|
9842
|
+
}
|
|
9843
|
+
const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
|
|
9844
|
+
const { proc, result } = spawnClaude({
|
|
9845
|
+
containerName,
|
|
9846
|
+
prompt: prompt2,
|
|
9847
|
+
allowedTools: "Read,Grep,Glob,Agent",
|
|
9848
|
+
model: "claude-opus-4-6",
|
|
9849
|
+
includePartialMessages: true,
|
|
9850
|
+
appendSystemPrompt: systemPrompt,
|
|
9851
|
+
resumeSessionId: this.board.sessionId || void 0,
|
|
9852
|
+
callbacks: {
|
|
9853
|
+
onLog: (entry) => sendLog(entry)
|
|
9854
|
+
}
|
|
9855
|
+
});
|
|
9856
|
+
this.board.process = proc;
|
|
9857
|
+
try {
|
|
9858
|
+
const { text, sessionId } = await result;
|
|
9859
|
+
if (sessionId) this.board.sessionId = sessionId;
|
|
9860
|
+
this.board.messages.push({ role: "assistant", content: text });
|
|
9861
|
+
return text;
|
|
9862
|
+
} finally {
|
|
9863
|
+
this.board.process = null;
|
|
9864
|
+
}
|
|
9865
|
+
}
|
|
9866
|
+
/** The persisted Ask AI conversation, for the client to hydrate on open. */
|
|
9867
|
+
boardSession() {
|
|
9868
|
+
return { messages: this.board.messages.slice() };
|
|
9869
|
+
}
|
|
9870
|
+
resetBoardSession() {
|
|
9871
|
+
if (this.board.process) {
|
|
9872
|
+
try {
|
|
9873
|
+
this.board.process.kill("SIGTERM");
|
|
9874
|
+
} catch {
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9877
|
+
this.board = { sessionId: null, process: null, messages: [] };
|
|
9878
|
+
}
|
|
9879
|
+
// ── Claude runner ──
|
|
9880
|
+
/** Strip git mentions from an agent prompt on OneDrive spaces; pass-through otherwise. */
|
|
9881
|
+
agentPrompt(text) {
|
|
9882
|
+
return this.core.isOneDrive(this.core.readConfig()) ? gitFree(text) : text;
|
|
9883
|
+
}
|
|
9884
|
+
async runClaudeInContainer(session, prompt2, tools, resumeSessionId, options) {
|
|
9885
|
+
const { proc, result } = spawnClaude({
|
|
9886
|
+
containerName: session.containerName,
|
|
9887
|
+
prompt: prompt2,
|
|
9888
|
+
allowedTools: tools,
|
|
9889
|
+
disallowedTools: options?.disallowedTools,
|
|
9890
|
+
permissionMode: options?.permissionMode,
|
|
9891
|
+
appendSystemPrompt: this.agentPrompt(buildPrompt(prompts_default.system)),
|
|
9892
|
+
effort: session.effort,
|
|
9893
|
+
model: "opus",
|
|
9894
|
+
resumeSessionId,
|
|
9895
|
+
callbacks: {
|
|
9896
|
+
onLog: (entry) => {
|
|
9897
|
+
if (entry.type === "text-append") {
|
|
9898
|
+
this.emitter.appendTextLog(session, entry.content);
|
|
9899
|
+
} else {
|
|
9900
|
+
this.emitter.addLog(session, entry.type, entry.content);
|
|
9901
|
+
}
|
|
9902
|
+
},
|
|
9903
|
+
onTodos: (todos) => {
|
|
9904
|
+
session.todos = todos;
|
|
9905
|
+
this.emitter.emitSessionUpdate(session);
|
|
9906
|
+
}
|
|
9907
|
+
}
|
|
9908
|
+
});
|
|
9909
|
+
session.proc = proc;
|
|
9910
|
+
try {
|
|
9911
|
+
return await result;
|
|
9912
|
+
} finally {
|
|
9913
|
+
session.proc = null;
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
// ── Research with Q&A ──
|
|
9917
|
+
async runResearch(session, prompt2, resumeSessionId) {
|
|
9918
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9919
|
+
const { text, sessionId } = await this.runClaudeInContainer(
|
|
9920
|
+
session,
|
|
9921
|
+
prompt2,
|
|
9922
|
+
"Read,Grep,Glob,Agent,TodoWrite",
|
|
9923
|
+
resumeSessionId,
|
|
9924
|
+
{ permissionMode: "plan", disallowedTools: "AskUserQuestion" }
|
|
9925
|
+
);
|
|
9926
|
+
session.claudeSessionId = sessionId;
|
|
9927
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9928
|
+
const markerIdx = text.lastIndexOf(_ClaudeModeEngine.QUESTION_MARKER);
|
|
9929
|
+
if (markerIdx === -1) return;
|
|
9930
|
+
session.phase = "awaiting-input";
|
|
9931
|
+
session.pendingQuestion = text.slice(markerIdx + _ClaudeModeEngine.QUESTION_MARKER.length).trim();
|
|
9932
|
+
this.emitter.emitSessionUpdate(session);
|
|
9933
|
+
this.emitter.addLog(
|
|
9934
|
+
session,
|
|
9935
|
+
"system",
|
|
9936
|
+
"Claude has a question - waiting for your input."
|
|
9937
|
+
);
|
|
9938
|
+
const answer = await new Promise((resolve) => {
|
|
9939
|
+
session.answerResolver = resolve;
|
|
9940
|
+
});
|
|
9941
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9942
|
+
session.phase = "research";
|
|
9943
|
+
session.pendingQuestion = "";
|
|
9944
|
+
session.answerResolver = null;
|
|
9945
|
+
this.emitter.emitSessionUpdate(session);
|
|
9946
|
+
this.emitter.addLog(session, "system", "Resuming research...");
|
|
9947
|
+
return this.runResearch(session, answer, sessionId);
|
|
9948
|
+
}
|
|
9949
|
+
// ── Pipeline ──
|
|
9950
|
+
async runPipeline(session, prompt2, baseBranch, runtimes) {
|
|
9951
|
+
if (!this.core.isReady()) {
|
|
9952
|
+
throw new Error(
|
|
9953
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9954
|
+
);
|
|
9955
|
+
}
|
|
9956
|
+
const config = this.core.readConfig();
|
|
9957
|
+
this.emitter.addLog(session, "system", "Starting container...");
|
|
9958
|
+
const onedrive = this.core.isOneDrive(config);
|
|
9959
|
+
await killContainer(session.containerName);
|
|
9960
|
+
await spawnContainer(session.containerName, config.claudeToken, {
|
|
9961
|
+
fuse: onedrive
|
|
9962
|
+
});
|
|
9963
|
+
if (onedrive) {
|
|
9964
|
+
const rotated = await writeRcloneMountConfig(
|
|
9965
|
+
session.containerName,
|
|
9966
|
+
this.core.getOneDriveConnection()
|
|
9967
|
+
);
|
|
9968
|
+
this.core.updateOneDriveRefreshToken(rotated);
|
|
9969
|
+
await mountWorkspace(session.containerName, this.core.remotePath());
|
|
9970
|
+
startOneDriveSyncWatcher(
|
|
9971
|
+
session.containerName,
|
|
9972
|
+
(pending, files) => this.emitter.emitOneDriveSync({
|
|
9973
|
+
postId: session.postId,
|
|
9974
|
+
pending,
|
|
9975
|
+
files
|
|
9976
|
+
})
|
|
9977
|
+
);
|
|
9978
|
+
this.emitter.addLog(session, "system", "OneDrive folder mounted.");
|
|
9979
|
+
} else {
|
|
9980
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
9981
|
+
const branch = baseBranch || config.mainBranch || "main";
|
|
9982
|
+
const cacheKey = repoCacheKey(config.repoUrl);
|
|
9983
|
+
const { fromCache } = await cloneRepo(
|
|
9984
|
+
session.containerName,
|
|
9985
|
+
cloneUrl,
|
|
9986
|
+
branch,
|
|
9987
|
+
cacheKey
|
|
9988
|
+
);
|
|
9989
|
+
this.emitter.addLog(
|
|
9990
|
+
session,
|
|
9991
|
+
"system",
|
|
9992
|
+
fromCache ? "Repository updated from cache." : "Repository cloned."
|
|
9993
|
+
);
|
|
9994
|
+
await setGitIdentity(
|
|
9995
|
+
session.containerName,
|
|
9996
|
+
config.gitName,
|
|
9997
|
+
config.gitEmail
|
|
9998
|
+
);
|
|
9999
|
+
}
|
|
10000
|
+
await installRuntimes(this.emitter, session, runtimes);
|
|
10001
|
+
this.emitter.addLog(session, "system", "Container ready.");
|
|
10002
|
+
session.phase = "research";
|
|
10003
|
+
session.step = 1;
|
|
10004
|
+
this.emitter.emitSessionUpdate(session);
|
|
10005
|
+
this.emitter.addLog(session, "system", "\u2500\u2500 Step 1: Research \u2500\u2500");
|
|
10006
|
+
await this.runResearch(
|
|
10007
|
+
session,
|
|
10008
|
+
buildPrompt(prompts_default.research, { task: prompt2 })
|
|
10009
|
+
);
|
|
10010
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10011
|
+
await this.runImplementation(
|
|
10012
|
+
session,
|
|
10013
|
+
buildPrompt(prompts_default.implement),
|
|
10014
|
+
"\u2500\u2500 Step"
|
|
10015
|
+
);
|
|
10016
|
+
}
|
|
10017
|
+
async runImplementation(session, implementPrompt, logPrefix) {
|
|
10018
|
+
const tools = _ClaudeModeEngine.FULL_TOOLS;
|
|
10019
|
+
session.phase = "implement";
|
|
10020
|
+
session.step = 2;
|
|
10021
|
+
this.emitter.emitSessionUpdate(session);
|
|
10022
|
+
this.emitter.addLog(session, "system", `${logPrefix} 2: Implement \u2500\u2500`);
|
|
10023
|
+
const implResult = await this.runClaudeInContainer(
|
|
10024
|
+
session,
|
|
10025
|
+
implementPrompt,
|
|
10026
|
+
tools,
|
|
10027
|
+
session.claudeSessionId
|
|
10028
|
+
);
|
|
10029
|
+
session.claudeSessionId = implResult.sessionId || session.claudeSessionId;
|
|
10030
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10031
|
+
session.phase = "review";
|
|
10032
|
+
session.step = 3;
|
|
10033
|
+
this.emitter.emitSessionUpdate(session);
|
|
10034
|
+
this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
|
|
10035
|
+
const reviewResult = await this.runClaudeInContainer(
|
|
10036
|
+
session,
|
|
10037
|
+
this.agentPrompt(buildPrompt(prompts_default.review)),
|
|
10038
|
+
tools,
|
|
10039
|
+
session.claudeSessionId
|
|
10040
|
+
);
|
|
10041
|
+
session.claudeSessionId = reviewResult.sessionId || session.claudeSessionId;
|
|
10042
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10043
|
+
session.reproSteps = reviewResult.text.trim();
|
|
10044
|
+
await this.extractAndComplete(session);
|
|
10045
|
+
}
|
|
10046
|
+
// On-demand dev-server preview: reuses the live container + Claude session,
|
|
10047
|
+
// maps the port (or a public frpc tunnel), stores the URL. Stays in `complete`.
|
|
10048
|
+
async runPreview(session) {
|
|
10049
|
+
const tools = _ClaudeModeEngine.FULL_TOOLS;
|
|
10050
|
+
session.previewRunning = true;
|
|
10051
|
+
this.emitter.addLog(session, "system", "Setting up live preview...");
|
|
10052
|
+
this.emitter.emitSessionUpdate(session);
|
|
10053
|
+
const portsEnv = (await execInContainer(session.containerName, "echo -n $FACTIII_PORTS")).trim();
|
|
10054
|
+
const publicPreviewEnabled = !!this.core.runnerToken;
|
|
10055
|
+
if (publicPreviewEnabled && this.core.runnerToken) {
|
|
10056
|
+
await installFrpcHarness({
|
|
10057
|
+
containerName: session.containerName,
|
|
10058
|
+
taskId: session.taskId,
|
|
10059
|
+
runnerToken: this.core.runnerToken
|
|
10060
|
+
});
|
|
10061
|
+
}
|
|
10062
|
+
const previewResult = await this.runClaudeInContainer(
|
|
10063
|
+
session,
|
|
10064
|
+
buildPrompt(prompts_default.preview, {
|
|
10065
|
+
ports: portsEnv,
|
|
10066
|
+
publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
|
|
10067
|
+
}),
|
|
10068
|
+
tools,
|
|
10069
|
+
session.claudeSessionId
|
|
10070
|
+
);
|
|
10071
|
+
session.claudeSessionId = previewResult.sessionId || session.claudeSessionId;
|
|
10072
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10073
|
+
const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
|
|
10074
|
+
const previewPort = portMatch ? portMatch[1] : "3000";
|
|
10075
|
+
const urlMatch = previewResult.text.match(/PREVIEW_URL:\s*(\S+)/);
|
|
10076
|
+
if (publicPreviewEnabled && urlMatch) {
|
|
10077
|
+
session.previewUrl = urlMatch[1];
|
|
10078
|
+
this.emitter.addLog(
|
|
10079
|
+
session,
|
|
10080
|
+
"system",
|
|
10081
|
+
`Public preview available at ${session.previewUrl}`
|
|
10082
|
+
);
|
|
10083
|
+
} else {
|
|
10084
|
+
const previewUrl = await getContainerPort(
|
|
10085
|
+
session.containerName,
|
|
10086
|
+
previewPort
|
|
10087
|
+
);
|
|
10088
|
+
if (previewUrl) {
|
|
10089
|
+
session.previewUrl = previewUrl;
|
|
10090
|
+
this.emitter.addLog(
|
|
10091
|
+
session,
|
|
10092
|
+
"system",
|
|
10093
|
+
`Preview available at ${session.previewUrl}`
|
|
10094
|
+
);
|
|
10095
|
+
} else {
|
|
10096
|
+
this.emitter.addLog(
|
|
10097
|
+
session,
|
|
10098
|
+
"system",
|
|
10099
|
+
"Could not map preview port. Server may not be running."
|
|
10100
|
+
);
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
this.emitter.emitSessionUpdate(session);
|
|
10104
|
+
}
|
|
10105
|
+
async extractAndComplete(session) {
|
|
10106
|
+
const config = this.core.readConfig();
|
|
10107
|
+
if (this.core.isOneDrive(config)) {
|
|
10108
|
+
session.diff = "";
|
|
10109
|
+
session.changedFiles = [];
|
|
10110
|
+
session.commitMessage = "";
|
|
10111
|
+
session.phase = "complete";
|
|
10112
|
+
this.emitter.emitSessionUpdate(session);
|
|
10113
|
+
return;
|
|
10114
|
+
}
|
|
10115
|
+
this.emitter.addLog(session, "system", "Extracting changes...");
|
|
10116
|
+
const { diff, changedFiles } = await extractDiff(session.containerName);
|
|
10117
|
+
session.diff = diff;
|
|
10118
|
+
session.changedFiles = changedFiles;
|
|
10119
|
+
if (changedFiles.length > 0) {
|
|
10120
|
+
this.emitter.addLog(
|
|
10121
|
+
session,
|
|
10122
|
+
"system",
|
|
10123
|
+
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
10124
|
+
);
|
|
10125
|
+
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
10126
|
+
session.commitMessage = await generateCommitMessage(
|
|
10127
|
+
session.containerName,
|
|
10128
|
+
diff
|
|
10129
|
+
);
|
|
10130
|
+
} else {
|
|
10131
|
+
this.emitter.addLog(session, "system", "No files changed.");
|
|
10132
|
+
}
|
|
10133
|
+
session.phase = "complete";
|
|
10134
|
+
this.emitter.emitSessionUpdate(session);
|
|
10135
|
+
}
|
|
10136
|
+
// ── Lifecycle / ModeEngine ──
|
|
10137
|
+
start(base, args) {
|
|
10138
|
+
const session = {
|
|
10139
|
+
...base,
|
|
10140
|
+
claudeSessionId: "",
|
|
10141
|
+
answerResolver: null,
|
|
10142
|
+
proc: null
|
|
10143
|
+
};
|
|
10144
|
+
this.sessions.set(session.postId, session);
|
|
10145
|
+
this.emitter.emitSessionUpdate(session);
|
|
10146
|
+
const onError = (err) => {
|
|
10147
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10148
|
+
session.phase = "error";
|
|
10149
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
10150
|
+
this.emitter.addLog(session, "error", session.error);
|
|
10151
|
+
this.emitter.emitSessionUpdate(session);
|
|
10152
|
+
};
|
|
10153
|
+
this.runPipeline(
|
|
10154
|
+
session,
|
|
10155
|
+
args.prompt,
|
|
10156
|
+
args.baseBranch,
|
|
10157
|
+
args.runtimes
|
|
10158
|
+
).catch(onError);
|
|
10159
|
+
}
|
|
10160
|
+
// Triggered from the review (complete) phase; returns the live preview URL.
|
|
10161
|
+
async cardStartPreview({
|
|
10162
|
+
postId: rawPostId
|
|
10163
|
+
}) {
|
|
10164
|
+
const postId = String(rawPostId);
|
|
10165
|
+
const session = this.sessions.get(postId);
|
|
10166
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10167
|
+
if (session.phase !== "complete")
|
|
10168
|
+
throw new Error("Preview is only available once changes are ready.");
|
|
10169
|
+
if (!session.claudeSessionId)
|
|
10170
|
+
throw new Error("No Claude session to resume for preview.");
|
|
10171
|
+
if (session.previewRunning || session.proc)
|
|
10172
|
+
return { previewUrl: session.previewUrl };
|
|
10173
|
+
try {
|
|
10174
|
+
await this.runPreview(session);
|
|
10175
|
+
} finally {
|
|
10176
|
+
session.previewRunning = false;
|
|
10177
|
+
if (this.sessions.has(postId)) this.emitter.emitSessionUpdate(session);
|
|
10178
|
+
}
|
|
10179
|
+
return { previewUrl: session.previewUrl };
|
|
10180
|
+
}
|
|
10181
|
+
cardAnswerQuestion({
|
|
10182
|
+
postId: rawPostId,
|
|
10183
|
+
answer
|
|
10184
|
+
}) {
|
|
10185
|
+
const postId = String(rawPostId);
|
|
10186
|
+
const session = this.sessions.get(postId);
|
|
10187
|
+
if (!session || session.phase !== "awaiting-input" || !session.answerResolver)
|
|
10188
|
+
return;
|
|
10189
|
+
session.answerResolver(answer);
|
|
10190
|
+
}
|
|
10191
|
+
cardFeedback({
|
|
10192
|
+
postId: rawPostId,
|
|
10193
|
+
feedback
|
|
10194
|
+
}) {
|
|
10195
|
+
const postId = String(rawPostId);
|
|
10196
|
+
const session = this.sessions.get(postId);
|
|
10197
|
+
if (!session || session.phase !== "complete") return;
|
|
10198
|
+
if (!session.claudeSessionId) return;
|
|
10199
|
+
session.diff = "";
|
|
10200
|
+
session.changedFiles = [];
|
|
10201
|
+
session.commitMessage = "";
|
|
10202
|
+
session.reproSteps = "";
|
|
10203
|
+
const prompt2 = `The user reviewed your changes and has the following feedback:
|
|
10204
|
+
|
|
10205
|
+
${feedback}
|
|
10206
|
+
|
|
10207
|
+
${buildPrompt(prompts_default.implement)}`;
|
|
10208
|
+
this.runImplementation(session, prompt2, "\u2500\u2500 Follow-up").catch((err) => {
|
|
10209
|
+
if (!this.sessions.has(postId)) return;
|
|
10210
|
+
session.phase = "error";
|
|
10211
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
10212
|
+
this.emitter.addLog(session, "error", session.error);
|
|
10213
|
+
this.emitter.emitSessionUpdate(session);
|
|
10214
|
+
});
|
|
10215
|
+
}
|
|
10216
|
+
listSummaries() {
|
|
10217
|
+
const result = {};
|
|
10218
|
+
for (const [postId, session] of this.sessions) {
|
|
10219
|
+
result[postId] = toSummary(session);
|
|
10220
|
+
}
|
|
10221
|
+
return result;
|
|
10222
|
+
}
|
|
10223
|
+
get(postId) {
|
|
10224
|
+
return this.sessions.get(postId);
|
|
10225
|
+
}
|
|
10226
|
+
sessionWithLogs(postId) {
|
|
10227
|
+
const session = this.sessions.get(postId);
|
|
10228
|
+
if (!session) return null;
|
|
10229
|
+
return { ...toSummary(session), logs: session.logs };
|
|
10230
|
+
}
|
|
10231
|
+
has(postId) {
|
|
10232
|
+
return this.sessions.has(postId);
|
|
10233
|
+
}
|
|
10234
|
+
isRunning(postId) {
|
|
10235
|
+
const s = this.sessions.get(postId);
|
|
10236
|
+
return !!s && (s.phase === "starting" || s.phase === "research" || s.phase === "implement");
|
|
10237
|
+
}
|
|
10238
|
+
remove(postId) {
|
|
10239
|
+
this.sessions.delete(postId);
|
|
10240
|
+
}
|
|
10241
|
+
async kill(postId) {
|
|
10242
|
+
let session = this.sessions.get(postId);
|
|
10243
|
+
if (!session) {
|
|
10244
|
+
for (const [, s] of this.sessions) {
|
|
10245
|
+
if (s.taskId === postId) {
|
|
10246
|
+
session = s;
|
|
10247
|
+
break;
|
|
10248
|
+
}
|
|
10249
|
+
}
|
|
10250
|
+
}
|
|
10251
|
+
if (!session) return;
|
|
10252
|
+
if (session.answerResolver) {
|
|
10253
|
+
session.answerResolver("");
|
|
10254
|
+
session.answerResolver = null;
|
|
10255
|
+
}
|
|
10256
|
+
if (session.proc) {
|
|
10257
|
+
try {
|
|
10258
|
+
session.proc.kill("SIGTERM");
|
|
10259
|
+
} catch {
|
|
10260
|
+
}
|
|
10261
|
+
session.proc = null;
|
|
10262
|
+
}
|
|
10263
|
+
await killContainer(session.containerName);
|
|
10264
|
+
const removedPostId = session.postId;
|
|
10265
|
+
this.sessions.delete(removedPostId);
|
|
10266
|
+
this.emitter.emitSessionRemoved(removedPostId);
|
|
10267
|
+
}
|
|
10268
|
+
destroy() {
|
|
10269
|
+
this.resetBoardSession();
|
|
10270
|
+
for (const [, session] of this.sessions) {
|
|
10271
|
+
if (session.proc) {
|
|
10272
|
+
try {
|
|
10273
|
+
session.proc.kill("SIGTERM");
|
|
10274
|
+
} catch {
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
void killContainer(session.containerName);
|
|
10278
|
+
}
|
|
10279
|
+
this.sessions.clear();
|
|
10280
|
+
}
|
|
10281
|
+
};
|
|
10282
|
+
|
|
10283
|
+
// ../../shared/all/helpers/terminal-ai/terminal-mode.ts
|
|
10284
|
+
var BARE_REPO_ROOT = "/home/claude/workspace";
|
|
10285
|
+
function posixNormalize(p) {
|
|
10286
|
+
const parts2 = p.split("/").filter(Boolean);
|
|
10287
|
+
const stack = [];
|
|
10288
|
+
for (const part of parts2) {
|
|
10289
|
+
if (part === ".") continue;
|
|
10290
|
+
if (part === "..") {
|
|
10291
|
+
if (stack.length) stack.pop();
|
|
10292
|
+
continue;
|
|
10293
|
+
}
|
|
10294
|
+
stack.push(part);
|
|
10295
|
+
}
|
|
10296
|
+
return "/" + stack.join("/");
|
|
10297
|
+
}
|
|
10298
|
+
function posixDirname(p) {
|
|
10299
|
+
const i = p.lastIndexOf("/");
|
|
10300
|
+
if (i <= 0) return "/";
|
|
10301
|
+
return p.slice(0, i);
|
|
10302
|
+
}
|
|
10303
|
+
function shEscape(s) {
|
|
10304
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10305
|
+
}
|
|
10306
|
+
var TerminalModeEngine = class {
|
|
10307
|
+
constructor(core, emitter) {
|
|
10308
|
+
this.core = core;
|
|
10309
|
+
this.emitter = emitter;
|
|
10310
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
10311
|
+
}
|
|
10312
|
+
// ── Lifecycle / ModeEngine ──
|
|
10313
|
+
start(base, args) {
|
|
10314
|
+
const session = {
|
|
10315
|
+
...base,
|
|
10316
|
+
openedFiles: [],
|
|
10317
|
+
buffers: {},
|
|
10318
|
+
exposed: []
|
|
10319
|
+
};
|
|
10320
|
+
this.sessions.set(session.postId, session);
|
|
10321
|
+
this.emitter.emitSessionUpdate(session);
|
|
10322
|
+
const onError = (err) => {
|
|
10323
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10324
|
+
session.phase = "error";
|
|
10325
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
10326
|
+
this.emitter.addLog(session, "error", session.error);
|
|
10327
|
+
this.emitter.emitSessionUpdate(session);
|
|
10328
|
+
};
|
|
10329
|
+
this.runBareSession(session, args.baseBranch, args.runtimes ?? []).catch(
|
|
10330
|
+
onError
|
|
10331
|
+
);
|
|
10332
|
+
}
|
|
10333
|
+
listSummaries() {
|
|
10334
|
+
const result = {};
|
|
10335
|
+
for (const [postId, session] of this.sessions) {
|
|
10336
|
+
result[postId] = toSummary(session);
|
|
10337
|
+
}
|
|
10338
|
+
return result;
|
|
10339
|
+
}
|
|
10340
|
+
get(postId) {
|
|
10341
|
+
return this.sessions.get(postId);
|
|
10342
|
+
}
|
|
10343
|
+
sessionWithLogs(postId) {
|
|
10344
|
+
const session = this.sessions.get(postId);
|
|
10345
|
+
if (!session) return null;
|
|
10346
|
+
return { ...toSummary(session), logs: session.logs };
|
|
10347
|
+
}
|
|
10348
|
+
has(postId) {
|
|
10349
|
+
return this.sessions.has(postId);
|
|
10350
|
+
}
|
|
10351
|
+
isRunning(postId) {
|
|
10352
|
+
const s = this.sessions.get(postId);
|
|
10353
|
+
return !!s && (s.phase === "starting" || s.phase === "bare-active");
|
|
10354
|
+
}
|
|
10355
|
+
remove(postId) {
|
|
10356
|
+
this.sessions.delete(postId);
|
|
10357
|
+
}
|
|
10358
|
+
async kill(postId) {
|
|
10359
|
+
let session = this.sessions.get(postId);
|
|
10360
|
+
if (!session) {
|
|
10361
|
+
for (const [, s] of this.sessions) {
|
|
10362
|
+
if (s.taskId === postId) {
|
|
10363
|
+
session = s;
|
|
10364
|
+
break;
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
if (!session) return;
|
|
10369
|
+
await killContainer(session.containerName);
|
|
10370
|
+
const removedPostId = session.postId;
|
|
10371
|
+
this.sessions.delete(removedPostId);
|
|
10372
|
+
this.emitter.emitSessionRemoved(removedPostId);
|
|
10373
|
+
}
|
|
10374
|
+
destroy() {
|
|
10375
|
+
for (const [, session] of this.sessions) {
|
|
10376
|
+
void killContainer(session.containerName);
|
|
10377
|
+
}
|
|
10378
|
+
this.sessions.clear();
|
|
10379
|
+
}
|
|
10380
|
+
/** Active container for a card, or null when none / terminated. */
|
|
10381
|
+
getCardContainerName(postId) {
|
|
10382
|
+
const session = this.sessions.get(String(postId));
|
|
10383
|
+
if (!session) return null;
|
|
10384
|
+
if (session.phase === "error" || session.phase === "done") return null;
|
|
10385
|
+
return session.containerName;
|
|
10386
|
+
}
|
|
10387
|
+
// ── container + Claude CLI, user drives ──
|
|
10388
|
+
async runBareSession(session, baseBranch, runtimes) {
|
|
10389
|
+
if (!this.core.isReady()) {
|
|
10390
|
+
throw new Error(
|
|
10391
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
10392
|
+
);
|
|
10393
|
+
}
|
|
10394
|
+
const config = this.core.readConfig();
|
|
10395
|
+
this.emitter.addLog(session, "system", "Starting container...");
|
|
10396
|
+
const onedrive = this.core.isOneDrive(config);
|
|
10397
|
+
await killContainer(session.containerName);
|
|
10398
|
+
await spawnContainer(session.containerName, void 0, { fuse: onedrive });
|
|
10399
|
+
if (onedrive) {
|
|
10400
|
+
const rotated = await writeRcloneMountConfig(
|
|
10401
|
+
session.containerName,
|
|
10402
|
+
this.core.getOneDriveConnection()
|
|
9300
10403
|
);
|
|
10404
|
+
this.core.updateOneDriveRefreshToken(rotated);
|
|
10405
|
+
await mountWorkspace(session.containerName, this.core.remotePath());
|
|
10406
|
+
startOneDriveSyncWatcher(
|
|
10407
|
+
session.containerName,
|
|
10408
|
+
(pending, files) => this.emitter.emitOneDriveSync({
|
|
10409
|
+
postId: session.postId,
|
|
10410
|
+
pending,
|
|
10411
|
+
files
|
|
10412
|
+
})
|
|
10413
|
+
);
|
|
10414
|
+
this.emitter.addLog(session, "system", "OneDrive folder mounted.");
|
|
10415
|
+
} else {
|
|
10416
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
10417
|
+
const branch = baseBranch || config.mainBranch || "main";
|
|
10418
|
+
const cacheKey = repoCacheKey(config.repoUrl);
|
|
10419
|
+
const { fromCache } = await cloneRepo(
|
|
10420
|
+
session.containerName,
|
|
10421
|
+
cloneUrl,
|
|
10422
|
+
branch,
|
|
10423
|
+
cacheKey
|
|
10424
|
+
);
|
|
10425
|
+
this.emitter.addLog(
|
|
10426
|
+
session,
|
|
10427
|
+
"system",
|
|
10428
|
+
fromCache ? "Repository updated from cache." : "Repository cloned."
|
|
10429
|
+
);
|
|
10430
|
+
await setGitIdentity(
|
|
10431
|
+
session.containerName,
|
|
10432
|
+
config.gitName,
|
|
10433
|
+
config.gitEmail
|
|
10434
|
+
);
|
|
10435
|
+
}
|
|
10436
|
+
if (this.core.runnerToken) {
|
|
10437
|
+
try {
|
|
10438
|
+
await installFrpcHarness({
|
|
10439
|
+
containerName: session.containerName,
|
|
10440
|
+
taskId: session.taskId,
|
|
10441
|
+
runnerToken: this.core.runnerToken
|
|
10442
|
+
});
|
|
10443
|
+
} catch (err) {
|
|
10444
|
+
this.emitter.addLog(
|
|
10445
|
+
session,
|
|
10446
|
+
"system",
|
|
10447
|
+
`Port forwarding unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
10448
|
+
);
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
await installRuntimes(this.emitter, session, runtimes);
|
|
10452
|
+
session.phase = "bare-active";
|
|
10453
|
+
this.emitter.emitSessionUpdate(session);
|
|
10454
|
+
this.emitter.addLog(
|
|
10455
|
+
session,
|
|
10456
|
+
"system",
|
|
10457
|
+
"Container ready. Drive it from the terminal."
|
|
10458
|
+
);
|
|
10459
|
+
}
|
|
10460
|
+
// ── file ops ──
|
|
10461
|
+
getBareContainer(postId) {
|
|
10462
|
+
const session = this.sessions.get(String(postId));
|
|
10463
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10464
|
+
if (session.mode !== "bare") {
|
|
10465
|
+
throw new Error("File ops are only available in Bare mode.");
|
|
10466
|
+
}
|
|
10467
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10468
|
+
throw new Error("Container is not ready.");
|
|
10469
|
+
}
|
|
10470
|
+
return session.containerName;
|
|
10471
|
+
}
|
|
10472
|
+
/**
|
|
10473
|
+
* Resolve a repo-relative path to an absolute container path, rejecting
|
|
10474
|
+
* anything that escapes the repo root.
|
|
10475
|
+
*/
|
|
10476
|
+
resolveBarePath(rel) {
|
|
10477
|
+
const normalized = posixNormalize("/" + (rel || "").replace(/^\/+/, ""));
|
|
10478
|
+
const abs = `${BARE_REPO_ROOT}${normalized === "/" ? "" : normalized}`;
|
|
10479
|
+
if (abs !== BARE_REPO_ROOT && !abs.startsWith(BARE_REPO_ROOT + "/")) {
|
|
10480
|
+
throw new Error("Path escapes repo root.");
|
|
10481
|
+
}
|
|
10482
|
+
return abs;
|
|
10483
|
+
}
|
|
10484
|
+
async bareFsList({
|
|
10485
|
+
postId,
|
|
10486
|
+
path: path7
|
|
10487
|
+
}) {
|
|
10488
|
+
const container = this.getBareContainer(postId);
|
|
10489
|
+
const abs = this.resolveBarePath(path7);
|
|
10490
|
+
const relRoot = abs.slice(BARE_REPO_ROOT.length) || "/";
|
|
10491
|
+
const out = await execInContainer(
|
|
10492
|
+
container,
|
|
10493
|
+
`cd ${shEscape(abs)} && for f in .* *; do
|
|
10494
|
+
case "$f" in
|
|
10495
|
+
"."|".."|".git"|"*") continue ;;
|
|
10496
|
+
esac
|
|
10497
|
+
[ -e "$f" ] || continue
|
|
10498
|
+
if [ -d "$f" ]; then echo "d/$f"; else echo "f/$f"; fi
|
|
10499
|
+
done | LC_ALL=C sort`
|
|
10500
|
+
);
|
|
10501
|
+
const entries = [];
|
|
10502
|
+
for (const line of out.split("\n")) {
|
|
10503
|
+
if (!line) continue;
|
|
10504
|
+
const sep = line.indexOf("/");
|
|
10505
|
+
if (sep < 0) continue;
|
|
10506
|
+
const ty = line.slice(0, sep);
|
|
10507
|
+
const name = line.slice(sep + 1);
|
|
10508
|
+
const type = ty === "d" ? "dir" : "file";
|
|
10509
|
+
entries.push({
|
|
10510
|
+
name,
|
|
10511
|
+
path: relRoot === "/" ? `/${name}` : `${relRoot}/${name}`,
|
|
10512
|
+
type
|
|
10513
|
+
});
|
|
10514
|
+
}
|
|
10515
|
+
entries.sort((a, b) => {
|
|
10516
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
10517
|
+
return a.name.localeCompare(b.name);
|
|
10518
|
+
});
|
|
10519
|
+
return entries;
|
|
10520
|
+
}
|
|
10521
|
+
async bareFsSearch({
|
|
10522
|
+
postId
|
|
10523
|
+
}) {
|
|
10524
|
+
const container = this.getBareContainer(postId);
|
|
10525
|
+
const root = BARE_REPO_ROOT;
|
|
10526
|
+
const out = await execInContainer(
|
|
10527
|
+
container,
|
|
10528
|
+
`cd ${shEscape(root)} && (git ls-files --cached --others --exclude-standard 2>/dev/null || find . -type f -not -path './.git/*' | sed 's|^\\./||') | head -n 5000`
|
|
10529
|
+
);
|
|
10530
|
+
const files = [];
|
|
10531
|
+
for (const line of out.split("\n")) {
|
|
10532
|
+
const p = line.trim();
|
|
10533
|
+
if (p) files.push("/" + p);
|
|
10534
|
+
}
|
|
10535
|
+
return files;
|
|
10536
|
+
}
|
|
10537
|
+
async bareFsRead({
|
|
10538
|
+
postId,
|
|
10539
|
+
path: path7
|
|
10540
|
+
}) {
|
|
10541
|
+
const container = this.getBareContainer(postId);
|
|
10542
|
+
const abs = this.resolveBarePath(path7);
|
|
10543
|
+
const b64 = (await execInContainer(container, `base64 -w 0 ${shEscape(abs)}`)).trim();
|
|
10544
|
+
return { content: b64, encoding: "base64" };
|
|
10545
|
+
}
|
|
10546
|
+
async bareFsWrite({
|
|
10547
|
+
postId,
|
|
10548
|
+
path: path7,
|
|
10549
|
+
content,
|
|
10550
|
+
encoding
|
|
10551
|
+
}) {
|
|
10552
|
+
const container = this.getBareContainer(postId);
|
|
10553
|
+
const abs = this.resolveBarePath(path7);
|
|
10554
|
+
const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
|
|
10555
|
+
if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
|
|
10556
|
+
throw new Error("Invalid base64 payload.");
|
|
10557
|
+
}
|
|
10558
|
+
await execInContainer(
|
|
10559
|
+
container,
|
|
10560
|
+
`mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
|
|
10561
|
+
);
|
|
10562
|
+
const session = this.sessions.get(String(postId));
|
|
10563
|
+
if (session) delete session.buffers[path7];
|
|
10564
|
+
}
|
|
10565
|
+
bareEditorState({
|
|
10566
|
+
postId: rawPostId
|
|
10567
|
+
}) {
|
|
10568
|
+
const postId = String(rawPostId);
|
|
10569
|
+
const session = this.sessions.get(postId);
|
|
10570
|
+
if (!session || session.mode !== "bare") {
|
|
10571
|
+
return { openedFiles: [], buffers: {} };
|
|
10572
|
+
}
|
|
10573
|
+
return {
|
|
10574
|
+
openedFiles: session.openedFiles.slice(),
|
|
10575
|
+
buffers: { ...session.buffers }
|
|
10576
|
+
};
|
|
10577
|
+
}
|
|
10578
|
+
bareOpenFile({
|
|
10579
|
+
postId: rawPostId,
|
|
10580
|
+
path: path7
|
|
10581
|
+
}) {
|
|
10582
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10583
|
+
if (!session || session.mode !== "bare") return;
|
|
10584
|
+
if (!session.openedFiles.includes(path7)) {
|
|
10585
|
+
session.openedFiles.push(path7);
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10588
|
+
bareCloseFile({
|
|
10589
|
+
postId: rawPostId,
|
|
10590
|
+
path: path7
|
|
10591
|
+
}) {
|
|
10592
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10593
|
+
if (!session || session.mode !== "bare") return;
|
|
10594
|
+
session.openedFiles = session.openedFiles.filter((p) => p !== path7);
|
|
10595
|
+
delete session.buffers[path7];
|
|
10596
|
+
}
|
|
10597
|
+
bareBufferSet({
|
|
10598
|
+
postId: rawPostId,
|
|
10599
|
+
path: path7,
|
|
10600
|
+
content
|
|
10601
|
+
}) {
|
|
10602
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10603
|
+
if (!session || session.mode !== "bare") return;
|
|
10604
|
+
session.buffers[path7] = content;
|
|
10605
|
+
}
|
|
10606
|
+
async bareFsDelete({
|
|
10607
|
+
postId,
|
|
10608
|
+
path: path7
|
|
10609
|
+
}) {
|
|
10610
|
+
const container = this.getBareContainer(postId);
|
|
10611
|
+
const abs = this.resolveBarePath(path7);
|
|
10612
|
+
if (abs === BARE_REPO_ROOT) {
|
|
10613
|
+
throw new Error("Refusing to delete the repo root.");
|
|
10614
|
+
}
|
|
10615
|
+
await execInContainer(container, `rm -rf ${shEscape(abs)}`);
|
|
10616
|
+
}
|
|
10617
|
+
async bareFsMkdir({
|
|
10618
|
+
postId,
|
|
10619
|
+
path: path7
|
|
10620
|
+
}) {
|
|
10621
|
+
const container = this.getBareContainer(postId);
|
|
10622
|
+
const abs = this.resolveBarePath(path7);
|
|
10623
|
+
await execInContainer(container, `mkdir -p ${shEscape(abs)}`);
|
|
10624
|
+
}
|
|
10625
|
+
async bareReview({
|
|
10626
|
+
postId: rawPostId
|
|
10627
|
+
}) {
|
|
10628
|
+
const postId = String(rawPostId);
|
|
10629
|
+
const session = this.sessions.get(postId);
|
|
10630
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10631
|
+
if (session.mode !== "bare") {
|
|
10632
|
+
throw new Error("bareReview is only valid in Bare mode.");
|
|
10633
|
+
}
|
|
10634
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10635
|
+
throw new Error("Container is not ready.");
|
|
10636
|
+
}
|
|
10637
|
+
const config = this.core.readConfig();
|
|
10638
|
+
if (this.core.isOneDrive(config)) {
|
|
10639
|
+
session.diff = "";
|
|
10640
|
+
session.changedFiles = [];
|
|
10641
|
+
session.commitMessage = "";
|
|
10642
|
+
session.phase = "bare-review";
|
|
10643
|
+
this.emitter.emitSessionUpdate(session);
|
|
10644
|
+
return { diff: "", changedFiles: [], commitMessage: "" };
|
|
10645
|
+
}
|
|
10646
|
+
this.emitter.addLog(session, "system", "Extracting changes...");
|
|
10647
|
+
const { diff, changedFiles } = await extractDiff(session.containerName);
|
|
10648
|
+
session.diff = diff;
|
|
10649
|
+
session.changedFiles = changedFiles;
|
|
10650
|
+
if (changedFiles.length > 0) {
|
|
10651
|
+
this.emitter.addLog(
|
|
10652
|
+
session,
|
|
10653
|
+
"system",
|
|
10654
|
+
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
10655
|
+
);
|
|
10656
|
+
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
10657
|
+
try {
|
|
10658
|
+
session.commitMessage = await generateCommitMessage(
|
|
10659
|
+
session.containerName,
|
|
10660
|
+
diff
|
|
10661
|
+
);
|
|
10662
|
+
} catch {
|
|
10663
|
+
session.commitMessage = "WIP: changes from bare session";
|
|
10664
|
+
}
|
|
10665
|
+
} else {
|
|
10666
|
+
session.commitMessage = "";
|
|
10667
|
+
this.emitter.addLog(session, "system", "No files changed.");
|
|
10668
|
+
}
|
|
10669
|
+
session.phase = "bare-review";
|
|
10670
|
+
this.emitter.emitSessionUpdate(session);
|
|
10671
|
+
return {
|
|
10672
|
+
diff: session.diff,
|
|
10673
|
+
changedFiles: session.changedFiles,
|
|
10674
|
+
commitMessage: session.commitMessage
|
|
10675
|
+
};
|
|
10676
|
+
}
|
|
10677
|
+
async bareGitStatus({
|
|
10678
|
+
postId: rawPostId
|
|
10679
|
+
}) {
|
|
10680
|
+
const postId = String(rawPostId);
|
|
10681
|
+
const session = this.sessions.get(postId);
|
|
10682
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10683
|
+
if (session.mode !== "bare") {
|
|
10684
|
+
throw new Error("bareGitStatus is only valid in Bare mode.");
|
|
10685
|
+
}
|
|
10686
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10687
|
+
throw new Error("Container is not ready.");
|
|
10688
|
+
}
|
|
10689
|
+
if (this.core.isOneDrive(this.core.readConfig())) {
|
|
10690
|
+
return { diff: "", changedFiles: [] };
|
|
10691
|
+
}
|
|
10692
|
+
return await extractDiff(session.containerName);
|
|
10693
|
+
}
|
|
10694
|
+
async bareListTerminals({
|
|
10695
|
+
postId: rawPostId
|
|
10696
|
+
}) {
|
|
10697
|
+
const postId = String(rawPostId);
|
|
10698
|
+
const session = this.sessions.get(postId);
|
|
10699
|
+
if (!session) return [];
|
|
10700
|
+
if (session.mode !== "bare") return [];
|
|
10701
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10702
|
+
return [];
|
|
10703
|
+
}
|
|
10704
|
+
const sanitizedPostId = postId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
10705
|
+
const prefix = `terminal-${sanitizedPostId}-`;
|
|
10706
|
+
try {
|
|
10707
|
+
const stdout = await execInContainer(
|
|
10708
|
+
session.containerName,
|
|
10709
|
+
`tmux list-panes -a -F '#{session_name}|#{pane_current_command}' 2>/dev/null || true`
|
|
10710
|
+
);
|
|
10711
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10712
|
+
for (const raw of stdout.split("\n")) {
|
|
10713
|
+
const line = raw.trim();
|
|
10714
|
+
if (!line.startsWith(prefix)) continue;
|
|
10715
|
+
const rest = line.slice(prefix.length);
|
|
10716
|
+
const sep = rest.indexOf("|");
|
|
10717
|
+
if (sep === -1) continue;
|
|
10718
|
+
const id = rest.slice(0, sep);
|
|
10719
|
+
const cmd = rest.slice(sep + 1).trim() || "bash";
|
|
10720
|
+
if (!byId.has(id)) byId.set(id, cmd);
|
|
10721
|
+
}
|
|
10722
|
+
return Array.from(byId, ([id, name]) => ({ id, name })).sort(
|
|
10723
|
+
(a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
|
|
10724
|
+
);
|
|
10725
|
+
} catch {
|
|
10726
|
+
return [];
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
// ── port forwarding (factiii-expose / factiii-unexpose) ──
|
|
10730
|
+
async bareExpose({
|
|
10731
|
+
postId,
|
|
10732
|
+
port,
|
|
10733
|
+
name
|
|
10734
|
+
}) {
|
|
10735
|
+
const container = this.getBareContainer(postId);
|
|
10736
|
+
const session = this.sessions.get(String(postId));
|
|
10737
|
+
const safeName = name.trim();
|
|
10738
|
+
if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
|
|
10739
|
+
throw new Error("Name must be letters, numbers, or dashes.");
|
|
10740
|
+
}
|
|
10741
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
10742
|
+
throw new Error("Port must be between 1 and 65535.");
|
|
10743
|
+
}
|
|
10744
|
+
const out = (await execInContainer(
|
|
10745
|
+
container,
|
|
10746
|
+
`factiii-expose ${port} ${shEscape(safeName)} 2>&1`
|
|
10747
|
+
)).trim();
|
|
10748
|
+
const url2 = out.split("\n").filter(Boolean).pop() || "";
|
|
10749
|
+
if (!/^https?:\/\//.test(url2)) {
|
|
10750
|
+
throw new Error(out || "Failed to expose port.");
|
|
10751
|
+
}
|
|
10752
|
+
const entry = { name: safeName, port, url: url2 };
|
|
10753
|
+
session.exposed = session.exposed.filter((e) => e.name !== safeName);
|
|
10754
|
+
session.exposed.push(entry);
|
|
10755
|
+
return entry;
|
|
10756
|
+
}
|
|
10757
|
+
async bareUnexpose({
|
|
10758
|
+
postId,
|
|
10759
|
+
name
|
|
10760
|
+
}) {
|
|
10761
|
+
const container = this.getBareContainer(postId);
|
|
10762
|
+
const session = this.sessions.get(String(postId));
|
|
10763
|
+
const safeName = name.trim();
|
|
10764
|
+
if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
|
|
10765
|
+
throw new Error("Invalid tunnel name.");
|
|
9301
10766
|
}
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
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
|
|
10767
|
+
await execInContainer(
|
|
10768
|
+
container,
|
|
10769
|
+
`factiii-unexpose ${shEscape(safeName)} 2>&1 || true`
|
|
9324
10770
|
);
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
10771
|
+
session.exposed = session.exposed.filter((e) => e.name !== safeName);
|
|
10772
|
+
}
|
|
10773
|
+
bareListExposed({
|
|
10774
|
+
postId
|
|
10775
|
+
}) {
|
|
10776
|
+
const session = this.sessions.get(String(postId));
|
|
10777
|
+
if (!session || session.mode !== "bare") return [];
|
|
10778
|
+
return session.exposed.slice();
|
|
10779
|
+
}
|
|
10780
|
+
};
|
|
10781
|
+
|
|
10782
|
+
// ../../shared/all/helpers/board-agent-core/engine.ts
|
|
10783
|
+
function sanitizeTaskId(taskId) {
|
|
10784
|
+
const sanitized = String(taskId).replace(/[^a-zA-Z0-9_.-]/g, "");
|
|
10785
|
+
if (!sanitized) throw new Error("Invalid taskId");
|
|
10786
|
+
return sanitized;
|
|
10787
|
+
}
|
|
10788
|
+
var BoardAgentEngine = class {
|
|
10789
|
+
constructor(options) {
|
|
10790
|
+
this.core = new ContainerCore(options);
|
|
10791
|
+
this.emitter = options.emitter;
|
|
10792
|
+
this.claude = new ClaudeModeEngine(this.core, this.emitter);
|
|
10793
|
+
this.terminal = new TerminalModeEngine(this.core, this.emitter);
|
|
10794
|
+
}
|
|
10795
|
+
/** The mode engine that currently owns this postId, if any. */
|
|
10796
|
+
owner(postId) {
|
|
10797
|
+
if (this.claude.has(postId)) return this.claude;
|
|
10798
|
+
if (this.terminal.has(postId)) return this.terminal;
|
|
10799
|
+
return null;
|
|
10800
|
+
}
|
|
10801
|
+
// ── Config / setup (→ core) ──
|
|
10802
|
+
getConfig() {
|
|
10803
|
+
return this.core.getConfig();
|
|
10804
|
+
}
|
|
10805
|
+
setConfig(partial) {
|
|
10806
|
+
return this.core.setConfig(partial);
|
|
10807
|
+
}
|
|
10808
|
+
clearToken(key) {
|
|
10809
|
+
return this.core.clearToken(key);
|
|
10810
|
+
}
|
|
10811
|
+
setupStatus() {
|
|
10812
|
+
return this.core.setupStatus();
|
|
10813
|
+
}
|
|
10814
|
+
ensureSetup() {
|
|
10815
|
+
return this.core.ensureSetup();
|
|
10816
|
+
}
|
|
10817
|
+
cardRemoteBranches() {
|
|
10818
|
+
return this.core.cardRemoteBranches();
|
|
10819
|
+
}
|
|
10820
|
+
// Start the host-run device-code flow and return the code to show the user.
|
|
10821
|
+
// The host polls Microsoft and pushes one `onedrive-auth` event when done.
|
|
10822
|
+
async onedriveStartDeviceCode(p) {
|
|
10823
|
+
const start = await startDeviceCode(p.clientId);
|
|
10824
|
+
console.log(
|
|
10825
|
+
`[onedrive] device-code started: userCode=${start.userCode} expiresIn=${start.expiresIn}s interval=${start.interval}s`
|
|
9333
10826
|
);
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
enablePreview
|
|
10827
|
+
void this.pollDeviceCodeUntilDone(
|
|
10828
|
+
p.clientId,
|
|
10829
|
+
start.deviceCode,
|
|
10830
|
+
start.interval,
|
|
10831
|
+
start.expiresIn
|
|
9340
10832
|
);
|
|
10833
|
+
return {
|
|
10834
|
+
userCode: start.userCode,
|
|
10835
|
+
verificationUri: start.verificationUri,
|
|
10836
|
+
expiresIn: start.expiresIn
|
|
10837
|
+
};
|
|
9341
10838
|
}
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
10839
|
+
async pollDeviceCodeUntilDone(clientId, deviceCode, interval, expiresIn) {
|
|
10840
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10841
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
10842
|
+
let wait = Math.max(interval, 5);
|
|
10843
|
+
while (Date.now() < deadline) {
|
|
10844
|
+
await sleep(wait * 1e3);
|
|
10845
|
+
let res;
|
|
10846
|
+
try {
|
|
10847
|
+
res = await pollDeviceCode(clientId, deviceCode);
|
|
10848
|
+
} catch (err) {
|
|
10849
|
+
console.log(`[onedrive] poll threw (will retry): ${String(err)}`);
|
|
10850
|
+
continue;
|
|
10851
|
+
}
|
|
10852
|
+
console.log(`[onedrive] poll status=${res.status}`);
|
|
10853
|
+
if (res.status === "pending") continue;
|
|
10854
|
+
if (res.status === "slow_down") {
|
|
10855
|
+
wait += 5;
|
|
10856
|
+
continue;
|
|
10857
|
+
}
|
|
10858
|
+
if (res.status === "complete") {
|
|
10859
|
+
try {
|
|
10860
|
+
this.core.saveOneDriveConnection({
|
|
10861
|
+
refreshToken: res.refreshToken,
|
|
10862
|
+
clientId,
|
|
10863
|
+
...res.appFolder
|
|
10864
|
+
});
|
|
10865
|
+
console.log("[onedrive] connection saved locally");
|
|
10866
|
+
} catch (err) {
|
|
10867
|
+
console.log(
|
|
10868
|
+
`[onedrive] saveOneDriveConnection failed: ${String(err)}`
|
|
10869
|
+
);
|
|
10870
|
+
this.emitter.emitOneDriveAuth({
|
|
10871
|
+
status: "error",
|
|
10872
|
+
message: "Failed to store the OneDrive connection on the host."
|
|
10873
|
+
});
|
|
10874
|
+
return;
|
|
10875
|
+
}
|
|
10876
|
+
this.emitter.emitOneDriveAuth({ status: "complete" });
|
|
10877
|
+
return;
|
|
10878
|
+
}
|
|
10879
|
+
this.emitter.emitOneDriveAuth({
|
|
10880
|
+
status: res.status,
|
|
10881
|
+
message: res.status === "error" ? res.message : void 0
|
|
10882
|
+
});
|
|
10883
|
+
return;
|
|
10884
|
+
}
|
|
10885
|
+
console.log("[onedrive] device code reached deadline without completion");
|
|
10886
|
+
this.emitter.emitOneDriveAuth({ status: "expired" });
|
|
10887
|
+
}
|
|
10888
|
+
onedriveDisconnect() {
|
|
10889
|
+
this.core.clearOneDriveConnection();
|
|
10890
|
+
return { success: true };
|
|
10891
|
+
}
|
|
10892
|
+
// Unmount the workspace (flushing writes) and persist the rotated refresh
|
|
10893
|
+
// token. No-op for non-OneDrive spaces or when nothing is mounted.
|
|
10894
|
+
async onedriveTeardown(p) {
|
|
10895
|
+
const postId = String(p.postId);
|
|
10896
|
+
const session = this.owner(postId)?.get(postId);
|
|
10897
|
+
if (!session) return { success: true };
|
|
10898
|
+
stopOneDriveSyncWatcher(session.containerName);
|
|
10899
|
+
if (!this.core.isOneDrive(this.core.readConfig())) {
|
|
10900
|
+
return { success: true };
|
|
10901
|
+
}
|
|
10902
|
+
try {
|
|
10903
|
+
await unmountWorkspace(session.containerName);
|
|
10904
|
+
} catch {
|
|
10905
|
+
}
|
|
10906
|
+
const refreshToken = await readbackRefreshToken(
|
|
10907
|
+
session.containerName
|
|
10908
|
+
).catch(() => null);
|
|
10909
|
+
if (refreshToken) this.core.updateOneDriveRefreshToken(refreshToken);
|
|
10910
|
+
return { success: true };
|
|
10911
|
+
}
|
|
10912
|
+
// Pending upload count to seed the UI (0 = synced); live updates come via the
|
|
10913
|
+
// onedrive-sync push. Synced for non-OneDrive spaces or idle sessions.
|
|
10914
|
+
onedriveStatus(p) {
|
|
10915
|
+
const postId = String(p.postId);
|
|
10916
|
+
const session = this.owner(postId)?.get(postId);
|
|
10917
|
+
if (!session || !this.core.isOneDrive(this.core.readConfig())) {
|
|
10918
|
+
return { syncing: false, pending: 0, files: [] };
|
|
10919
|
+
}
|
|
10920
|
+
const pending = onedrivePendingUploads(session.containerName);
|
|
10921
|
+
const files = onedriveUploadingFiles(session.containerName);
|
|
10922
|
+
return { syncing: pending > 0, pending, files };
|
|
10923
|
+
}
|
|
10924
|
+
// ── Ask AI + claude-only controls (→ claude) ──
|
|
10925
|
+
ask(params, sendLog) {
|
|
10926
|
+
return this.claude.ask(params, sendLog);
|
|
10927
|
+
}
|
|
10928
|
+
resetBoardSession() {
|
|
10929
|
+
this.claude.resetBoardSession();
|
|
10930
|
+
}
|
|
10931
|
+
boardSession() {
|
|
10932
|
+
return this.claude.boardSession();
|
|
10933
|
+
}
|
|
10934
|
+
cardStartPreview(payload) {
|
|
10935
|
+
return this.claude.cardStartPreview(payload);
|
|
10936
|
+
}
|
|
10937
|
+
cardAnswerQuestion(payload) {
|
|
10938
|
+
this.claude.cardAnswerQuestion(payload);
|
|
10939
|
+
}
|
|
10940
|
+
cardFeedback(payload) {
|
|
10941
|
+
this.claude.cardFeedback(payload);
|
|
10942
|
+
}
|
|
10943
|
+
// ── Terminal ops (→ terminal) ──
|
|
10944
|
+
bareFsList(p) {
|
|
10945
|
+
return this.terminal.bareFsList(p);
|
|
10946
|
+
}
|
|
10947
|
+
bareFsSearch(p) {
|
|
10948
|
+
return this.terminal.bareFsSearch(p);
|
|
10949
|
+
}
|
|
10950
|
+
bareFsRead(p) {
|
|
10951
|
+
return this.terminal.bareFsRead(p);
|
|
10952
|
+
}
|
|
10953
|
+
bareFsWrite(p) {
|
|
10954
|
+
return this.terminal.bareFsWrite(p);
|
|
10955
|
+
}
|
|
10956
|
+
bareFsDelete(p) {
|
|
10957
|
+
return this.terminal.bareFsDelete(p);
|
|
10958
|
+
}
|
|
10959
|
+
bareFsMkdir(p) {
|
|
10960
|
+
return this.terminal.bareFsMkdir(p);
|
|
10961
|
+
}
|
|
10962
|
+
bareReview(p) {
|
|
10963
|
+
return this.terminal.bareReview(p);
|
|
10964
|
+
}
|
|
10965
|
+
bareGitStatus(p) {
|
|
10966
|
+
return this.terminal.bareGitStatus(p);
|
|
10967
|
+
}
|
|
10968
|
+
bareListTerminals(p) {
|
|
10969
|
+
return this.terminal.bareListTerminals(p);
|
|
10970
|
+
}
|
|
10971
|
+
bareEditorState(p) {
|
|
10972
|
+
return this.terminal.bareEditorState(p);
|
|
10973
|
+
}
|
|
10974
|
+
bareOpenFile(p) {
|
|
10975
|
+
this.terminal.bareOpenFile(p);
|
|
10976
|
+
}
|
|
10977
|
+
bareCloseFile(p) {
|
|
10978
|
+
this.terminal.bareCloseFile(p);
|
|
10979
|
+
}
|
|
10980
|
+
bareBufferSet(p) {
|
|
10981
|
+
this.terminal.bareBufferSet(p);
|
|
10982
|
+
}
|
|
10983
|
+
bareExpose(p) {
|
|
10984
|
+
return this.terminal.bareExpose(p);
|
|
10985
|
+
}
|
|
10986
|
+
bareUnexpose(p) {
|
|
10987
|
+
return this.terminal.bareUnexpose(p);
|
|
10988
|
+
}
|
|
10989
|
+
bareListExposed(p) {
|
|
10990
|
+
return this.terminal.bareListExposed(p);
|
|
10991
|
+
}
|
|
10992
|
+
// ── Cross-mode coordinators ──
|
|
10993
|
+
async cardExecute(payload) {
|
|
10994
|
+
const {
|
|
10995
|
+
postId: rawPostId,
|
|
10996
|
+
prompt: prompt2,
|
|
10997
|
+
baseBranch,
|
|
10998
|
+
targetBranch,
|
|
10999
|
+
taskTitle,
|
|
11000
|
+
effort,
|
|
11001
|
+
mode,
|
|
11002
|
+
runtimes
|
|
11003
|
+
} = payload;
|
|
9352
11004
|
const postId = String(rawPostId);
|
|
9353
|
-
|
|
9354
|
-
if (existing && (existing.phase === "starting" || existing.phase === "research" || existing.phase === "implement")) {
|
|
11005
|
+
if (this.claude.isRunning(postId) || this.terminal.isRunning(postId)) {
|
|
9355
11006
|
throw new Error("A session is already running for this card.");
|
|
9356
11007
|
}
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
11008
|
+
const stale = this.owner(postId);
|
|
11009
|
+
if (stale) {
|
|
11010
|
+
const s = stale.get(postId);
|
|
11011
|
+
await killContainer(s.containerName);
|
|
11012
|
+
stale.remove(postId);
|
|
9360
11013
|
}
|
|
9361
11014
|
const taskId = sanitizeTaskId(`${postId}-${Date.now()}`);
|
|
9362
|
-
const
|
|
11015
|
+
const base = {
|
|
9363
11016
|
taskId,
|
|
9364
11017
|
postId,
|
|
9365
11018
|
phase: "starting",
|
|
11019
|
+
mode,
|
|
9366
11020
|
step: 1,
|
|
9367
11021
|
logs: [],
|
|
9368
11022
|
diff: "",
|
|
9369
11023
|
changedFiles: [],
|
|
9370
11024
|
commitMessage: "",
|
|
9371
11025
|
previewUrl: "",
|
|
11026
|
+
previewRunning: false,
|
|
9372
11027
|
reproSteps: "",
|
|
9373
11028
|
pendingQuestion: "",
|
|
9374
11029
|
error: "",
|
|
@@ -9377,196 +11032,47 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9377
11032
|
targetBranch: targetBranch || "",
|
|
9378
11033
|
effort: effort || "high",
|
|
9379
11034
|
todos: [],
|
|
9380
|
-
containerName: `factiii-${this.instancePrefix}-card-${taskId}
|
|
9381
|
-
claudeSessionId: "",
|
|
9382
|
-
answerResolver: null,
|
|
9383
|
-
previewResolver: null
|
|
11035
|
+
containerName: `factiii-${this.core.instancePrefix}-card-${taskId}`
|
|
9384
11036
|
};
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
prompt2,
|
|
9390
|
-
enablePreview ?? false,
|
|
9391
|
-
baseBranch ?? ""
|
|
9392
|
-
).catch((err) => {
|
|
9393
|
-
if (!this.sessions.has(postId)) return;
|
|
9394
|
-
session.phase = "error";
|
|
9395
|
-
session.error = err instanceof Error ? err.message : String(err);
|
|
9396
|
-
this.emitter.addLog(session, "error", session.error);
|
|
9397
|
-
this.emitter.emitSessionUpdate(session);
|
|
9398
|
-
});
|
|
9399
|
-
return { taskId, postId };
|
|
9400
|
-
}
|
|
9401
|
-
static {
|
|
9402
|
-
this.FULL_TOOLS = "Read,Grep,Glob,Agent,Edit,Write,Bash,TodoWrite";
|
|
9403
|
-
}
|
|
9404
|
-
async runImplementation(session, implementPrompt, logPrefix, enablePreview = false) {
|
|
9405
|
-
const tools = _BoardAgentEngine.FULL_TOOLS;
|
|
9406
|
-
session.phase = "implement";
|
|
9407
|
-
session.step = 2;
|
|
9408
|
-
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;
|
|
9421
|
-
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) {
|
|
9426
|
-
await installFrpcHarness({
|
|
9427
|
-
containerName: session.containerName,
|
|
9428
|
-
taskId: session.taskId,
|
|
9429
|
-
runnerToken: this.runnerToken
|
|
9430
|
-
});
|
|
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];
|
|
9448
|
-
this.emitter.addLog(
|
|
9449
|
-
session,
|
|
9450
|
-
"system",
|
|
9451
|
-
`Public preview available at ${session.previewUrl}`
|
|
9452
|
-
);
|
|
9453
|
-
} else {
|
|
9454
|
-
const previewUrl = await getContainerPort(
|
|
9455
|
-
session.containerName,
|
|
9456
|
-
previewPort
|
|
9457
|
-
);
|
|
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
|
-
}
|
|
9473
|
-
this.emitter.emitSessionUpdate(session);
|
|
9474
|
-
await new Promise((resolve) => {
|
|
9475
|
-
session.previewResolver = resolve;
|
|
11037
|
+
if (mode === "bare") {
|
|
11038
|
+
this.terminal.start(base, {
|
|
11039
|
+
baseBranch: baseBranch ?? "",
|
|
11040
|
+
runtimes: runtimes ?? []
|
|
9476
11041
|
});
|
|
9477
|
-
session.previewResolver = null;
|
|
9478
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9479
|
-
}
|
|
9480
|
-
session.phase = "review";
|
|
9481
|
-
session.step = 3;
|
|
9482
|
-
this.emitter.emitSessionUpdate(session);
|
|
9483
|
-
this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
|
|
9484
|
-
const reviewResult = await this.runClaudeInContainer(
|
|
9485
|
-
session,
|
|
9486
|
-
buildPrompt(prompts_default.review),
|
|
9487
|
-
tools,
|
|
9488
|
-
session.claudeSessionId
|
|
9489
|
-
);
|
|
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
|
-
}
|
|
9495
|
-
async extractAndComplete(session) {
|
|
9496
|
-
this.emitter.addLog(session, "system", "Extracting changes...");
|
|
9497
|
-
const { diff, changedFiles } = await extractDiff(session.containerName);
|
|
9498
|
-
session.diff = diff;
|
|
9499
|
-
session.changedFiles = changedFiles;
|
|
9500
|
-
if (changedFiles.length > 0) {
|
|
9501
|
-
this.emitter.addLog(
|
|
9502
|
-
session,
|
|
9503
|
-
"system",
|
|
9504
|
-
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
9505
|
-
);
|
|
9506
|
-
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
9507
|
-
session.commitMessage = await generateCommitMessage(
|
|
9508
|
-
session.containerName,
|
|
9509
|
-
diff
|
|
9510
|
-
);
|
|
9511
11042
|
} else {
|
|
9512
|
-
this.
|
|
11043
|
+
this.claude.start(base, {
|
|
11044
|
+
prompt: prompt2,
|
|
11045
|
+
baseBranch: baseBranch ?? "",
|
|
11046
|
+
runtimes: runtimes ?? []
|
|
11047
|
+
});
|
|
9513
11048
|
}
|
|
9514
|
-
|
|
9515
|
-
this.emitter.emitSessionUpdate(session);
|
|
11049
|
+
return { taskId, postId };
|
|
9516
11050
|
}
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
if (!session
|
|
9524
|
-
|
|
9525
|
-
session.
|
|
11051
|
+
/**
|
|
11052
|
+
* Look up the active container for a card. Used by the terminal bridge to
|
|
11053
|
+
* map postId to the docker container it should attach a shell to.
|
|
11054
|
+
*/
|
|
11055
|
+
getCardContainerName(postId) {
|
|
11056
|
+
const session = this.claude.get(String(postId)) ?? this.terminal.get(String(postId));
|
|
11057
|
+
if (!session) return null;
|
|
11058
|
+
if (session.phase === "error" || session.phase === "done") return null;
|
|
11059
|
+
return session.containerName;
|
|
9526
11060
|
}
|
|
9527
11061
|
cardSessions() {
|
|
9528
|
-
|
|
9529
|
-
for (const [postId, session] of this.sessions) {
|
|
9530
|
-
result[postId] = toSummary(session);
|
|
9531
|
-
}
|
|
9532
|
-
return result;
|
|
11062
|
+
return { ...this.terminal.listSummaries(), ...this.claude.listSummaries() };
|
|
9533
11063
|
}
|
|
9534
|
-
cardSession({
|
|
9535
|
-
postId
|
|
9536
|
-
}) {
|
|
9537
|
-
const session = this.sessions.get(String(postId));
|
|
9538
|
-
if (!session) return null;
|
|
9539
|
-
return { ...toSummary(session), logs: session.logs };
|
|
11064
|
+
cardSession({ postId }) {
|
|
11065
|
+
return this.claude.sessionWithLogs(String(postId)) ?? this.terminal.sessionWithLogs(String(postId));
|
|
9540
11066
|
}
|
|
9541
11067
|
async cardDismiss({ postId }) {
|
|
9542
|
-
const
|
|
9543
|
-
if (!
|
|
11068
|
+
const owner = this.owner(String(postId));
|
|
11069
|
+
if (!owner) return;
|
|
11070
|
+
const session = owner.get(String(postId));
|
|
11071
|
+
await this.onedriveTeardown({ postId: String(postId) });
|
|
9544
11072
|
await killContainer(session.containerName);
|
|
9545
|
-
|
|
11073
|
+
owner.remove(String(postId));
|
|
9546
11074
|
this.emitter.emitSessionRemoved(String(postId));
|
|
9547
11075
|
}
|
|
9548
|
-
setupStatus() {
|
|
9549
|
-
if (this.containerName) return { phase: "ready" };
|
|
9550
|
-
if (this.setupPromise) return { phase: "running" };
|
|
9551
|
-
return { phase: "idle" };
|
|
9552
|
-
}
|
|
9553
|
-
async ensureSetup() {
|
|
9554
|
-
await this.ensureContainer();
|
|
9555
|
-
return { ready: true };
|
|
9556
|
-
}
|
|
9557
|
-
isReady() {
|
|
9558
|
-
return this.containerName !== null;
|
|
9559
|
-
}
|
|
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
|
-
);
|
|
9567
|
-
}
|
|
9568
|
-
return listRemoteBranches(this.containerName);
|
|
9569
|
-
}
|
|
9570
11076
|
async cardFinalize({
|
|
9571
11077
|
postId: rawPostId,
|
|
9572
11078
|
apply,
|
|
@@ -9574,128 +11080,79 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9574
11080
|
commitMessage
|
|
9575
11081
|
}) {
|
|
9576
11082
|
const postId = String(rawPostId);
|
|
9577
|
-
const
|
|
9578
|
-
if (!
|
|
11083
|
+
const owner = this.owner(postId);
|
|
11084
|
+
if (!owner) throw new Error("No active session for this card.");
|
|
11085
|
+
const session = owner.get(postId);
|
|
11086
|
+
const config = this.core.readConfig();
|
|
11087
|
+
if (this.core.isOneDrive(config)) {
|
|
11088
|
+
if (apply) {
|
|
11089
|
+
session.phase = "done";
|
|
11090
|
+
this.emitter.addLog(session, "system", "Changes are live on OneDrive.");
|
|
11091
|
+
this.emitter.emitSessionUpdate(session);
|
|
11092
|
+
return { applied: true, branchName: "" };
|
|
11093
|
+
}
|
|
11094
|
+
await this.onedriveTeardown({ postId });
|
|
11095
|
+
await killContainer(session.containerName);
|
|
11096
|
+
owner.remove(postId);
|
|
11097
|
+
this.emitter.emitSessionRemoved(postId);
|
|
11098
|
+
return { applied: false, branchName: "" };
|
|
11099
|
+
}
|
|
9579
11100
|
if (apply && branchName) {
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
11101
|
+
let pushResult;
|
|
11102
|
+
try {
|
|
11103
|
+
pushResult = await commitAndPush(
|
|
11104
|
+
session.containerName,
|
|
11105
|
+
branchName,
|
|
11106
|
+
commitMessage || `Board AI: ${branchName}`,
|
|
11107
|
+
{
|
|
11108
|
+
gitName: config.gitName,
|
|
11109
|
+
gitEmail: config.gitEmail
|
|
11110
|
+
}
|
|
11111
|
+
);
|
|
11112
|
+
} catch (err) {
|
|
11113
|
+
if (err instanceof NoChangesError) {
|
|
11114
|
+
this.emitter.addLog(
|
|
11115
|
+
session,
|
|
11116
|
+
"system",
|
|
11117
|
+
"No changes to push - container kept alive for inspection."
|
|
11118
|
+
);
|
|
11119
|
+
} else {
|
|
11120
|
+
this.emitter.addLog(
|
|
11121
|
+
session,
|
|
11122
|
+
"error",
|
|
11123
|
+
`Push failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11124
|
+
);
|
|
9588
11125
|
}
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
if (apply) {
|
|
11126
|
+
this.emitter.emitSessionUpdate(session);
|
|
11127
|
+
throw err;
|
|
11128
|
+
}
|
|
9593
11129
|
session.phase = "done";
|
|
9594
11130
|
this.emitter.addLog(
|
|
9595
11131
|
session,
|
|
9596
11132
|
"system",
|
|
9597
|
-
`
|
|
11133
|
+
`Pushed ${pushResult.pushedSha.slice(0, 8)} to origin/${pushResult.branch}`
|
|
9598
11134
|
);
|
|
9599
11135
|
this.emitter.emitSessionUpdate(session);
|
|
9600
|
-
|
|
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);
|
|
11136
|
+
return { applied: true, branchName: pushResult.branch };
|
|
9609
11137
|
}
|
|
11138
|
+
await killContainer(session.containerName);
|
|
11139
|
+
owner.remove(postId);
|
|
11140
|
+
this.emitter.emitSessionRemoved(postId);
|
|
9610
11141
|
return { applied: apply, branchName: branchName || "" };
|
|
9611
11142
|
}
|
|
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
11143
|
async cardKill({ postId: rawPostId }) {
|
|
9648
11144
|
const postId = String(rawPostId);
|
|
9649
|
-
|
|
9650
|
-
if (
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
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);
|
|
11145
|
+
await this.onedriveTeardown({ postId });
|
|
11146
|
+
if (this.claude.has(postId)) return this.claude.kill(postId);
|
|
11147
|
+
if (this.terminal.has(postId)) return this.terminal.kill(postId);
|
|
11148
|
+
await this.claude.kill(postId);
|
|
11149
|
+
await this.terminal.kill(postId);
|
|
9679
11150
|
}
|
|
9680
11151
|
// ── Lifecycle ──
|
|
9681
11152
|
destroy() {
|
|
9682
|
-
this.
|
|
9683
|
-
|
|
9684
|
-
|
|
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
|
-
}
|
|
11153
|
+
this.claude.destroy();
|
|
11154
|
+
this.terminal.destroy();
|
|
11155
|
+
this.core.destroy();
|
|
9699
11156
|
}
|
|
9700
11157
|
/**
|
|
9701
11158
|
* Route a single action/payload pair to the right engine method.
|
|
@@ -9725,13 +11182,16 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9725
11182
|
case "resetBoardSession":
|
|
9726
11183
|
this.resetBoardSession();
|
|
9727
11184
|
return void 0;
|
|
11185
|
+
case "boardSession":
|
|
11186
|
+
return this.boardSession();
|
|
9728
11187
|
case "cardExecute":
|
|
9729
11188
|
return await this.cardExecute(
|
|
9730
11189
|
payload
|
|
9731
11190
|
);
|
|
9732
|
-
case "
|
|
9733
|
-
this.
|
|
9734
|
-
|
|
11191
|
+
case "cardStartPreview":
|
|
11192
|
+
return await this.cardStartPreview(
|
|
11193
|
+
payload
|
|
11194
|
+
);
|
|
9735
11195
|
case "cardSessions":
|
|
9736
11196
|
return this.cardSessions();
|
|
9737
11197
|
case "cardSession":
|
|
@@ -9747,6 +11207,20 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9747
11207
|
return await this.ensureSetup();
|
|
9748
11208
|
case "cardRemoteBranches":
|
|
9749
11209
|
return await this.cardRemoteBranches();
|
|
11210
|
+
case "onedriveStartDeviceCode":
|
|
11211
|
+
return await this.onedriveStartDeviceCode(
|
|
11212
|
+
payload
|
|
11213
|
+
);
|
|
11214
|
+
case "onedriveDisconnect":
|
|
11215
|
+
return this.onedriveDisconnect();
|
|
11216
|
+
case "onedriveTeardown":
|
|
11217
|
+
return await this.onedriveTeardown(
|
|
11218
|
+
payload
|
|
11219
|
+
);
|
|
11220
|
+
case "onedriveStatus":
|
|
11221
|
+
return this.onedriveStatus(
|
|
11222
|
+
payload
|
|
11223
|
+
);
|
|
9750
11224
|
case "cardFinalize":
|
|
9751
11225
|
return await this.cardFinalize(
|
|
9752
11226
|
payload
|
|
@@ -9760,6 +11234,77 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9760
11234
|
case "cardKill":
|
|
9761
11235
|
await this.cardKill(payload);
|
|
9762
11236
|
return void 0;
|
|
11237
|
+
case "bareFsList":
|
|
11238
|
+
return await this.bareFsList(
|
|
11239
|
+
payload
|
|
11240
|
+
);
|
|
11241
|
+
case "bareFsSearch":
|
|
11242
|
+
return await this.bareFsSearch(
|
|
11243
|
+
payload
|
|
11244
|
+
);
|
|
11245
|
+
case "bareFsRead":
|
|
11246
|
+
return await this.bareFsRead(
|
|
11247
|
+
payload
|
|
11248
|
+
);
|
|
11249
|
+
case "bareFsWrite":
|
|
11250
|
+
await this.bareFsWrite(
|
|
11251
|
+
payload
|
|
11252
|
+
);
|
|
11253
|
+
return void 0;
|
|
11254
|
+
case "bareFsDelete":
|
|
11255
|
+
await this.bareFsDelete(
|
|
11256
|
+
payload
|
|
11257
|
+
);
|
|
11258
|
+
return void 0;
|
|
11259
|
+
case "bareFsMkdir":
|
|
11260
|
+
await this.bareFsMkdir(
|
|
11261
|
+
payload
|
|
11262
|
+
);
|
|
11263
|
+
return void 0;
|
|
11264
|
+
case "bareReview":
|
|
11265
|
+
return await this.bareReview(
|
|
11266
|
+
payload
|
|
11267
|
+
);
|
|
11268
|
+
case "bareGitStatus":
|
|
11269
|
+
return await this.bareGitStatus(
|
|
11270
|
+
payload
|
|
11271
|
+
);
|
|
11272
|
+
case "bareListTerminals":
|
|
11273
|
+
return await this.bareListTerminals(
|
|
11274
|
+
payload
|
|
11275
|
+
);
|
|
11276
|
+
case "bareEditorState":
|
|
11277
|
+
return this.bareEditorState(
|
|
11278
|
+
payload
|
|
11279
|
+
);
|
|
11280
|
+
case "bareOpenFile":
|
|
11281
|
+
this.bareOpenFile(
|
|
11282
|
+
payload
|
|
11283
|
+
);
|
|
11284
|
+
return void 0;
|
|
11285
|
+
case "bareCloseFile":
|
|
11286
|
+
this.bareCloseFile(
|
|
11287
|
+
payload
|
|
11288
|
+
);
|
|
11289
|
+
return void 0;
|
|
11290
|
+
case "bareBufferSet":
|
|
11291
|
+
this.bareBufferSet(
|
|
11292
|
+
payload
|
|
11293
|
+
);
|
|
11294
|
+
return void 0;
|
|
11295
|
+
case "bareExpose":
|
|
11296
|
+
return await this.bareExpose(
|
|
11297
|
+
payload
|
|
11298
|
+
);
|
|
11299
|
+
case "bareUnexpose":
|
|
11300
|
+
await this.bareUnexpose(
|
|
11301
|
+
payload
|
|
11302
|
+
);
|
|
11303
|
+
return void 0;
|
|
11304
|
+
case "bareListExposed":
|
|
11305
|
+
return this.bareListExposed(
|
|
11306
|
+
payload
|
|
11307
|
+
);
|
|
9763
11308
|
default:
|
|
9764
11309
|
throw new Error(`Unknown board-agent action: ${String(action)}`);
|
|
9765
11310
|
}
|
|
@@ -9816,7 +11361,7 @@ function makeChunkReassembler() {
|
|
|
9816
11361
|
|
|
9817
11362
|
// src/daemon.ts
|
|
9818
11363
|
var import_node_datachannel = require("node-datachannel");
|
|
9819
|
-
var
|
|
11364
|
+
var import_path5 = __toESM(require("path"));
|
|
9820
11365
|
|
|
9821
11366
|
// ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
|
|
9822
11367
|
var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
|
|
@@ -10530,8 +12075,8 @@ var BaseXHR = class extends Polling {
|
|
|
10530
12075
|
/**
|
|
10531
12076
|
* Sends data.
|
|
10532
12077
|
*
|
|
10533
|
-
* @param {String} data to send.
|
|
10534
|
-
* @param {Function} called upon flush.
|
|
12078
|
+
* @param {String} data - data to send.
|
|
12079
|
+
* @param {Function} fn - called upon flush.
|
|
10535
12080
|
* @private
|
|
10536
12081
|
*/
|
|
10537
12082
|
doWrite(data, fn) {
|
|
@@ -10749,10 +12294,13 @@ var XHR = class extends BaseXHR {
|
|
|
10749
12294
|
}
|
|
10750
12295
|
};
|
|
10751
12296
|
|
|
10752
|
-
// ../../node_modules/
|
|
12297
|
+
// ../../node_modules/ws/wrapper.mjs
|
|
10753
12298
|
var import_stream = __toESM(require_stream(), 1);
|
|
12299
|
+
var import_extension = __toESM(require_extension(), 1);
|
|
12300
|
+
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
10754
12301
|
var import_receiver = __toESM(require_receiver(), 1);
|
|
10755
12302
|
var import_sender = __toESM(require_sender(), 1);
|
|
12303
|
+
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
10756
12304
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
10757
12305
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
10758
12306
|
|
|
@@ -10993,12 +12541,12 @@ function parse2(str) {
|
|
|
10993
12541
|
uri.queryKey = queryKey(uri, uri["query"]);
|
|
10994
12542
|
return uri;
|
|
10995
12543
|
}
|
|
10996
|
-
function pathNames(obj,
|
|
10997
|
-
const regx = /\/{2,9}/g, names =
|
|
10998
|
-
if (
|
|
12544
|
+
function pathNames(obj, path7) {
|
|
12545
|
+
const regx = /\/{2,9}/g, names = path7.replace(regx, "/").split("/");
|
|
12546
|
+
if (path7.slice(0, 1) == "/" || path7.length === 0) {
|
|
10999
12547
|
names.splice(0, 1);
|
|
11000
12548
|
}
|
|
11001
|
-
if (
|
|
12549
|
+
if (path7.slice(-1) == "/") {
|
|
11002
12550
|
names.splice(names.length - 1, 1);
|
|
11003
12551
|
}
|
|
11004
12552
|
return names;
|
|
@@ -11353,7 +12901,7 @@ var SocketWithoutUpgrade = class _SocketWithoutUpgrade extends import_component_
|
|
|
11353
12901
|
/**
|
|
11354
12902
|
* Sends a packet.
|
|
11355
12903
|
*
|
|
11356
|
-
* @param {String} type
|
|
12904
|
+
* @param {String} type - packet type.
|
|
11357
12905
|
* @param {String} data.
|
|
11358
12906
|
* @param {Object} options.
|
|
11359
12907
|
* @param {Function} fn - callback function.
|
|
@@ -11602,11 +13150,12 @@ var SocketWithUpgrade = class extends SocketWithoutUpgrade {
|
|
|
11602
13150
|
};
|
|
11603
13151
|
var Socket = class extends SocketWithUpgrade {
|
|
11604
13152
|
constructor(uri, opts = {}) {
|
|
11605
|
-
const
|
|
13153
|
+
const isOptionsOnly = typeof uri === "object";
|
|
13154
|
+
const o = isOptionsOnly ? { ...uri } : { ...opts };
|
|
11606
13155
|
if (!o.transports || o.transports && typeof o.transports[0] === "string") {
|
|
11607
13156
|
o.transports = (o.transports || ["polling", "websocket", "webtransport"]).map((transportName) => transports[transportName]).filter((t) => !!t);
|
|
11608
13157
|
}
|
|
11609
|
-
super(uri, o);
|
|
13158
|
+
super(isOptionsOnly ? o : uri, o);
|
|
11610
13159
|
}
|
|
11611
13160
|
};
|
|
11612
13161
|
|
|
@@ -11616,7 +13165,7 @@ var protocol2 = Socket.protocol;
|
|
|
11616
13165
|
// ../../node_modules/socket.io-client/build/esm-debug/url.js
|
|
11617
13166
|
var import_debug7 = __toESM(require_src(), 1);
|
|
11618
13167
|
var debug7 = (0, import_debug7.default)("socket.io-client:url");
|
|
11619
|
-
function url(uri,
|
|
13168
|
+
function url(uri, path7 = "", loc) {
|
|
11620
13169
|
let obj = uri;
|
|
11621
13170
|
loc = loc || typeof location !== "undefined" && location;
|
|
11622
13171
|
if (null == uri)
|
|
@@ -11650,7 +13199,7 @@ function url(uri, path6 = "", loc) {
|
|
|
11650
13199
|
obj.path = obj.path || "/";
|
|
11651
13200
|
const ipv6 = obj.host.indexOf(":") !== -1;
|
|
11652
13201
|
const host = ipv6 ? "[" + obj.host + "]" : obj.host;
|
|
11653
|
-
obj.id = obj.protocol + "://" + host + ":" + obj.port +
|
|
13202
|
+
obj.id = obj.protocol + "://" + host + ":" + obj.port + path7;
|
|
11654
13203
|
obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
|
|
11655
13204
|
return obj;
|
|
11656
13205
|
}
|
|
@@ -13284,8 +14833,8 @@ function lookup(uri, opts) {
|
|
|
13284
14833
|
const parsed = url(uri, opts.path || "/socket.io");
|
|
13285
14834
|
const source = parsed.source;
|
|
13286
14835
|
const id = parsed.id;
|
|
13287
|
-
const
|
|
13288
|
-
const sameNamespace = cache[id] &&
|
|
14836
|
+
const path7 = parsed.path;
|
|
14837
|
+
const sameNamespace = cache[id] && path7 in cache[id]["nsps"];
|
|
13289
14838
|
const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
|
|
13290
14839
|
let io;
|
|
13291
14840
|
if (newConnection) {
|
|
@@ -13310,44 +14859,296 @@ Object.assign(lookup, {
|
|
|
13310
14859
|
connect: lookup
|
|
13311
14860
|
});
|
|
13312
14861
|
|
|
13313
|
-
//
|
|
14862
|
+
// ../../shared/all/helpers/terminal-ai/terminal.ts
|
|
14863
|
+
var import_child_process6 = require("child_process");
|
|
14864
|
+
var import_dockerode = __toESM(require("dockerode"));
|
|
13314
14865
|
var import_fs3 = __toESM(require("fs"));
|
|
13315
14866
|
var import_os2 = __toESM(require("os"));
|
|
13316
14867
|
var import_path3 = __toESM(require("path"));
|
|
13317
|
-
|
|
14868
|
+
function resolveDocker() {
|
|
14869
|
+
if (process.env.DOCKER_HOST) return new import_dockerode.default();
|
|
14870
|
+
try {
|
|
14871
|
+
const out = (0, import_child_process6.execFileSync)(
|
|
14872
|
+
"docker",
|
|
14873
|
+
["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
|
|
14874
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
14875
|
+
).trim();
|
|
14876
|
+
if (out.startsWith("unix://")) {
|
|
14877
|
+
const p = out.slice("unix://".length);
|
|
14878
|
+
if (import_fs3.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
|
|
14879
|
+
} else if (out) {
|
|
14880
|
+
process.env.DOCKER_HOST = out;
|
|
14881
|
+
return new import_dockerode.default();
|
|
14882
|
+
}
|
|
14883
|
+
} catch {
|
|
14884
|
+
}
|
|
14885
|
+
const candidates = [
|
|
14886
|
+
"/var/run/docker.sock",
|
|
14887
|
+
import_path3.default.join(import_os2.default.homedir(), ".docker/run/docker.sock"),
|
|
14888
|
+
import_path3.default.join(import_os2.default.homedir(), ".docker/desktop/docker.sock"),
|
|
14889
|
+
import_path3.default.join(import_os2.default.homedir(), ".orbstack/run/docker.sock"),
|
|
14890
|
+
import_path3.default.join(import_os2.default.homedir(), ".colima/default/docker.sock"),
|
|
14891
|
+
import_path3.default.join(import_os2.default.homedir(), ".rd/docker.sock")
|
|
14892
|
+
];
|
|
14893
|
+
for (const p of candidates) {
|
|
14894
|
+
try {
|
|
14895
|
+
import_fs3.default.statSync(p);
|
|
14896
|
+
return new import_dockerode.default({ socketPath: p });
|
|
14897
|
+
} catch {
|
|
14898
|
+
}
|
|
14899
|
+
}
|
|
14900
|
+
return new import_dockerode.default();
|
|
14901
|
+
}
|
|
14902
|
+
var docker = resolveDocker();
|
|
14903
|
+
var TERMINAL_INIT_SCRIPT = [
|
|
14904
|
+
`export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`,
|
|
14905
|
+
`exec bash --norc -i`
|
|
14906
|
+
].join("; ");
|
|
14907
|
+
function tmuxName(terminalKey) {
|
|
14908
|
+
const safe = terminalKey.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
14909
|
+
return `terminal-${safe}`;
|
|
14910
|
+
}
|
|
14911
|
+
var TerminalManager = class {
|
|
14912
|
+
constructor(send, getCardContainerName) {
|
|
14913
|
+
this.send = send;
|
|
14914
|
+
this.getCardContainerName = getCardContainerName;
|
|
14915
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
14916
|
+
}
|
|
14917
|
+
handle(msg) {
|
|
14918
|
+
switch (msg.op) {
|
|
14919
|
+
case "open":
|
|
14920
|
+
void this.open(msg);
|
|
14921
|
+
return;
|
|
14922
|
+
case "stdin":
|
|
14923
|
+
this.stdin(msg.sessionId, msg.data);
|
|
14924
|
+
return;
|
|
14925
|
+
case "resize":
|
|
14926
|
+
void this.resize(msg.sessionId, msg.cols, msg.rows);
|
|
14927
|
+
return;
|
|
14928
|
+
case "close":
|
|
14929
|
+
this.close(msg.sessionId);
|
|
14930
|
+
return;
|
|
14931
|
+
case "kill":
|
|
14932
|
+
void this.killTmuxSession(msg.postId, msg.terminalKey);
|
|
14933
|
+
return;
|
|
14934
|
+
}
|
|
14935
|
+
}
|
|
14936
|
+
async open(msg) {
|
|
14937
|
+
if (this.sessions.has(msg.sessionId)) {
|
|
14938
|
+
this.emit({
|
|
14939
|
+
type: "terminal",
|
|
14940
|
+
op: "error",
|
|
14941
|
+
sessionId: msg.sessionId,
|
|
14942
|
+
message: "Session already open."
|
|
14943
|
+
});
|
|
14944
|
+
return;
|
|
14945
|
+
}
|
|
14946
|
+
const containerName = this.getCardContainerName(msg.postId);
|
|
14947
|
+
if (!containerName) {
|
|
14948
|
+
this.emit({
|
|
14949
|
+
type: "terminal",
|
|
14950
|
+
op: "error",
|
|
14951
|
+
sessionId: msg.sessionId,
|
|
14952
|
+
message: `No active card session for postId=${msg.postId}.`
|
|
14953
|
+
});
|
|
14954
|
+
return;
|
|
14955
|
+
}
|
|
14956
|
+
const tmuxSession = tmuxName(msg.terminalKey);
|
|
14957
|
+
try {
|
|
14958
|
+
const exec = await docker.getContainer(containerName).exec({
|
|
14959
|
+
Cmd: [
|
|
14960
|
+
"tmux",
|
|
14961
|
+
// -u forces UTF-8 regardless of the locale. Slim base images often
|
|
14962
|
+
// ship without a UTF-8 locale, so tmux's auto-detect falls through to
|
|
14963
|
+
// ASCII line-draw chars (the "qqqq" mess in place of ─).
|
|
14964
|
+
"-u",
|
|
14965
|
+
"new-session",
|
|
14966
|
+
"-A",
|
|
14967
|
+
"-s",
|
|
14968
|
+
tmuxSession,
|
|
14969
|
+
"--",
|
|
14970
|
+
"bash",
|
|
14971
|
+
"-c",
|
|
14972
|
+
TERMINAL_INIT_SCRIPT
|
|
14973
|
+
],
|
|
14974
|
+
AttachStdin: true,
|
|
14975
|
+
AttachStdout: true,
|
|
14976
|
+
AttachStderr: true,
|
|
14977
|
+
Tty: true,
|
|
14978
|
+
// Drop the user into the cloned repo so claude/git/etc. just work.
|
|
14979
|
+
WorkingDir: "/home/claude/workspace",
|
|
14980
|
+
Env: ["TERM=xterm-256color"],
|
|
14981
|
+
// The client follows up with a resize op carrying real dimensions.
|
|
14982
|
+
ConsoleSize: [msg.rows, msg.cols]
|
|
14983
|
+
});
|
|
14984
|
+
const stream = await exec.start({
|
|
14985
|
+
hijack: true,
|
|
14986
|
+
stdin: true,
|
|
14987
|
+
Tty: true
|
|
14988
|
+
});
|
|
14989
|
+
this.sessions.set(msg.sessionId, { exec, stream, containerName });
|
|
14990
|
+
stream.on("data", (chunk) => {
|
|
14991
|
+
this.emit({
|
|
14992
|
+
type: "terminal",
|
|
14993
|
+
op: "stdout",
|
|
14994
|
+
sessionId: msg.sessionId,
|
|
14995
|
+
data: chunk.toString("base64")
|
|
14996
|
+
});
|
|
14997
|
+
});
|
|
14998
|
+
stream.on("error", (err) => {
|
|
14999
|
+
this.cleanup(msg.sessionId);
|
|
15000
|
+
this.emit({
|
|
15001
|
+
type: "terminal",
|
|
15002
|
+
op: "error",
|
|
15003
|
+
sessionId: msg.sessionId,
|
|
15004
|
+
message: err.message
|
|
15005
|
+
});
|
|
15006
|
+
});
|
|
15007
|
+
stream.on("end", () => {
|
|
15008
|
+
void this.finalize(msg.sessionId, exec);
|
|
15009
|
+
});
|
|
15010
|
+
stream.on("close", () => {
|
|
15011
|
+
void this.finalize(msg.sessionId, exec);
|
|
15012
|
+
});
|
|
15013
|
+
this.emit({
|
|
15014
|
+
type: "terminal",
|
|
15015
|
+
op: "opened",
|
|
15016
|
+
sessionId: msg.sessionId,
|
|
15017
|
+
containerName
|
|
15018
|
+
});
|
|
15019
|
+
} catch (err) {
|
|
15020
|
+
this.emit({
|
|
15021
|
+
type: "terminal",
|
|
15022
|
+
op: "error",
|
|
15023
|
+
sessionId: msg.sessionId,
|
|
15024
|
+
message: err instanceof Error ? err.message : String(err)
|
|
15025
|
+
});
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
stdin(sessionId, b64) {
|
|
15029
|
+
const session = this.sessions.get(sessionId);
|
|
15030
|
+
if (!session) return;
|
|
15031
|
+
try {
|
|
15032
|
+
session.stream.write(Buffer.from(b64, "base64"));
|
|
15033
|
+
} catch {
|
|
15034
|
+
}
|
|
15035
|
+
}
|
|
15036
|
+
async resize(sessionId, cols, rows) {
|
|
15037
|
+
const session = this.sessions.get(sessionId);
|
|
15038
|
+
if (!session) return;
|
|
15039
|
+
try {
|
|
15040
|
+
await session.exec.resize({ h: rows, w: cols });
|
|
15041
|
+
} catch {
|
|
15042
|
+
}
|
|
15043
|
+
}
|
|
15044
|
+
close(sessionId) {
|
|
15045
|
+
const session = this.sessions.get(sessionId);
|
|
15046
|
+
if (!session) return;
|
|
15047
|
+
try {
|
|
15048
|
+
session.stream.end();
|
|
15049
|
+
} catch {
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
/** Tear down the tmux session for a tab. Used when the user explicitly
|
|
15053
|
+
* removes a tab (the X button), not on transient detach. */
|
|
15054
|
+
async killTmuxSession(postId, terminalKey) {
|
|
15055
|
+
const containerName = this.getCardContainerName(postId);
|
|
15056
|
+
if (!containerName) return;
|
|
15057
|
+
const session = tmuxName(terminalKey);
|
|
15058
|
+
try {
|
|
15059
|
+
const exec = await docker.getContainer(containerName).exec({
|
|
15060
|
+
Cmd: ["tmux", "kill-session", "-t", session],
|
|
15061
|
+
AttachStdout: false,
|
|
15062
|
+
AttachStderr: false,
|
|
15063
|
+
Tty: false
|
|
15064
|
+
});
|
|
15065
|
+
await exec.start({});
|
|
15066
|
+
} catch {
|
|
15067
|
+
}
|
|
15068
|
+
}
|
|
15069
|
+
destroyAll() {
|
|
15070
|
+
for (const sessionId of [...this.sessions.keys()]) {
|
|
15071
|
+
this.close(sessionId);
|
|
15072
|
+
}
|
|
15073
|
+
}
|
|
15074
|
+
// Called only once even if both 'end' and 'close' fire.
|
|
15075
|
+
async finalize(sessionId, exec) {
|
|
15076
|
+
if (!this.sessions.has(sessionId)) return;
|
|
15077
|
+
this.cleanup(sessionId);
|
|
15078
|
+
let code = null;
|
|
15079
|
+
try {
|
|
15080
|
+
const info = await exec.inspect();
|
|
15081
|
+
code = typeof info.ExitCode === "number" ? info.ExitCode : null;
|
|
15082
|
+
} catch {
|
|
15083
|
+
}
|
|
15084
|
+
this.emit({ type: "terminal", op: "exit", sessionId, code });
|
|
15085
|
+
}
|
|
15086
|
+
cleanup(sessionId) {
|
|
15087
|
+
this.sessions.delete(sessionId);
|
|
15088
|
+
}
|
|
15089
|
+
emit(msg) {
|
|
15090
|
+
const wire = JSON.stringify(msg);
|
|
15091
|
+
for (const frame of chunkWire(wire)) this.send(frame);
|
|
15092
|
+
}
|
|
15093
|
+
};
|
|
15094
|
+
|
|
15095
|
+
// src/agent-adapter.ts
|
|
15096
|
+
var import_fs4 = __toESM(require("fs"));
|
|
15097
|
+
var import_os3 = __toESM(require("os"));
|
|
15098
|
+
var import_path4 = __toESM(require("path"));
|
|
15099
|
+
var CONFIG_DIR2 = import_path4.default.join(import_os3.default.homedir(), ".factiii-runner");
|
|
15100
|
+
function safeSlug(spaceSlug) {
|
|
15101
|
+
return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
15102
|
+
}
|
|
13318
15103
|
function configPath(spaceSlug) {
|
|
13319
|
-
|
|
13320
|
-
|
|
15104
|
+
return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.json`);
|
|
15105
|
+
}
|
|
15106
|
+
function connectionPath(spaceSlug) {
|
|
15107
|
+
return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
|
|
15108
|
+
}
|
|
15109
|
+
function writeJson(filePath, value2) {
|
|
15110
|
+
import_fs4.default.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
15111
|
+
import_fs4.default.writeFileSync(filePath, JSON.stringify(value2, null, 2));
|
|
15112
|
+
import_fs4.default.chmodSync(filePath, 384);
|
|
13321
15113
|
}
|
|
13322
15114
|
var localConfigProvider = {
|
|
13323
15115
|
readConfig(spaceSlug) {
|
|
15116
|
+
let config = {};
|
|
13324
15117
|
try {
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
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
|
-
};
|
|
15118
|
+
config = JSON.parse(
|
|
15119
|
+
import_fs4.default.readFileSync(configPath(spaceSlug), "utf-8")
|
|
15120
|
+
);
|
|
13335
15121
|
} catch {
|
|
13336
|
-
return {
|
|
13337
|
-
repoUrl: "",
|
|
13338
|
-
mainBranch: "main",
|
|
13339
|
-
gitName: "",
|
|
13340
|
-
gitEmail: "",
|
|
13341
|
-
claudeToken: "",
|
|
13342
|
-
githubToken: ""
|
|
13343
|
-
};
|
|
13344
15122
|
}
|
|
15123
|
+
return {
|
|
15124
|
+
repoUrl: config.repoUrl || "",
|
|
15125
|
+
mainBranch: config.mainBranch || "main",
|
|
15126
|
+
gitName: config.gitName || "",
|
|
15127
|
+
gitEmail: config.gitEmail || "",
|
|
15128
|
+
claudeToken: config.claudeToken || "",
|
|
15129
|
+
githubToken: config.githubToken || "",
|
|
15130
|
+
storageBackend: config.storageBackend || "github"
|
|
15131
|
+
};
|
|
13345
15132
|
},
|
|
13346
15133
|
writeConfig(spaceSlug, config) {
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
15134
|
+
writeJson(configPath(spaceSlug), config);
|
|
15135
|
+
},
|
|
15136
|
+
readOneDriveConnection(spaceSlug) {
|
|
15137
|
+
try {
|
|
15138
|
+
return JSON.parse(
|
|
15139
|
+
import_fs4.default.readFileSync(connectionPath(spaceSlug), "utf-8")
|
|
15140
|
+
);
|
|
15141
|
+
} catch {
|
|
15142
|
+
return null;
|
|
15143
|
+
}
|
|
15144
|
+
},
|
|
15145
|
+
writeOneDriveConnection(spaceSlug, connection) {
|
|
15146
|
+
const filePath = connectionPath(spaceSlug);
|
|
15147
|
+
if (!connection) {
|
|
15148
|
+
import_fs4.default.rmSync(filePath, { force: true });
|
|
15149
|
+
return;
|
|
15150
|
+
}
|
|
15151
|
+
writeJson(filePath, connection);
|
|
13351
15152
|
}
|
|
13352
15153
|
};
|
|
13353
15154
|
|
|
@@ -13362,6 +15163,14 @@ async function startDaemon(config) {
|
|
|
13362
15163
|
});
|
|
13363
15164
|
const engines = /* @__PURE__ */ new Map();
|
|
13364
15165
|
const openChannels = /* @__PURE__ */ new Set();
|
|
15166
|
+
const terminalByChannel = /* @__PURE__ */ new WeakMap();
|
|
15167
|
+
const findCardContainer = (postId) => {
|
|
15168
|
+
for (const engine of engines.values()) {
|
|
15169
|
+
const name = engine.getCardContainerName(postId);
|
|
15170
|
+
if (name) return name;
|
|
15171
|
+
}
|
|
15172
|
+
return null;
|
|
15173
|
+
};
|
|
13365
15174
|
const broadcastToAll = (msg) => {
|
|
13366
15175
|
const frames = chunkWire(msg);
|
|
13367
15176
|
for (const dc of openChannels) {
|
|
@@ -13469,12 +15278,45 @@ async function startDaemon(config) {
|
|
|
13469
15278
|
}
|
|
13470
15279
|
}
|
|
13471
15280
|
};
|
|
15281
|
+
const sendRawToChannel = (frame) => {
|
|
15282
|
+
try {
|
|
15283
|
+
dc.sendMessage(frame);
|
|
15284
|
+
} catch {
|
|
15285
|
+
}
|
|
15286
|
+
};
|
|
15287
|
+
terminalByChannel.set(
|
|
15288
|
+
dc,
|
|
15289
|
+
new TerminalManager(sendRawToChannel, findCardContainer)
|
|
15290
|
+
);
|
|
15291
|
+
dc.onOpen(() => {
|
|
15292
|
+
for (const [spaceSlug, engine] of engines.entries()) {
|
|
15293
|
+
const sessions = engine.cardSessions();
|
|
15294
|
+
for (const summary of Object.values(sessions)) {
|
|
15295
|
+
const msg = JSON.stringify({
|
|
15296
|
+
type: "event",
|
|
15297
|
+
spaceSlug,
|
|
15298
|
+
event: `board-agent:${spaceSlug}:session-update`,
|
|
15299
|
+
payload: summary
|
|
15300
|
+
});
|
|
15301
|
+
sendToChannel(msg);
|
|
15302
|
+
}
|
|
15303
|
+
}
|
|
15304
|
+
});
|
|
13472
15305
|
dc.onMessage((raw) => {
|
|
13473
15306
|
const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
13474
15307
|
const msgStr = reassembler.feed(rawStr);
|
|
13475
15308
|
if (msgStr === null) return;
|
|
13476
15309
|
try {
|
|
13477
15310
|
const parsed = JSON.parse(msgStr);
|
|
15311
|
+
if (parsed.type === "terminal") {
|
|
15312
|
+
const mgr = terminalByChannel.get(dc);
|
|
15313
|
+
if (mgr) {
|
|
15314
|
+
mgr.handle(
|
|
15315
|
+
parsed
|
|
15316
|
+
);
|
|
15317
|
+
}
|
|
15318
|
+
return;
|
|
15319
|
+
}
|
|
13478
15320
|
if (parsed.type === "broadcast" && parsed.spaceSlug && parsed.event) {
|
|
13479
15321
|
const eventMsg = JSON.stringify({
|
|
13480
15322
|
type: "event",
|
|
@@ -13506,6 +15348,11 @@ async function startDaemon(config) {
|
|
|
13506
15348
|
dc.onClosed(() => {
|
|
13507
15349
|
console.log(`Data channel closed with ${data.fromSocketId}`);
|
|
13508
15350
|
openChannels.delete(dc);
|
|
15351
|
+
const mgr = terminalByChannel.get(dc);
|
|
15352
|
+
if (mgr) {
|
|
15353
|
+
mgr.destroyAll();
|
|
15354
|
+
terminalByChannel.delete(dc);
|
|
15355
|
+
}
|
|
13509
15356
|
detachIceHandler();
|
|
13510
15357
|
});
|
|
13511
15358
|
});
|
|
@@ -13578,16 +15425,18 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
|
|
|
13578
15425
|
}
|
|
13579
15426
|
}
|
|
13580
15427
|
function getDockerfilePath() {
|
|
13581
|
-
return
|
|
15428
|
+
return import_path5.default.join(__dirname, "..", "Dockerfile.claude");
|
|
13582
15429
|
}
|
|
13583
15430
|
|
|
13584
15431
|
// src/setup.ts
|
|
13585
|
-
var
|
|
13586
|
-
var
|
|
13587
|
-
var
|
|
15432
|
+
var import_child_process7 = require("child_process");
|
|
15433
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
15434
|
+
var import_fs5 = __toESM(require("fs"));
|
|
15435
|
+
var import_path6 = __toESM(require("path"));
|
|
15436
|
+
var DOCKERFILE_LABEL2 = "factiii.dockerfile-sha";
|
|
13588
15437
|
function run(bin, args, opts) {
|
|
13589
15438
|
try {
|
|
13590
|
-
return (0,
|
|
15439
|
+
return (0, import_child_process7.execFileSync)(bin, args, {
|
|
13591
15440
|
encoding: "utf-8",
|
|
13592
15441
|
stdio: opts?.silent ? "pipe" : "inherit",
|
|
13593
15442
|
// Avoids a cmd.exe window flash on Windows during silent checks.
|
|
@@ -13600,7 +15449,7 @@ function run(bin, args, opts) {
|
|
|
13600
15449
|
function check(cmd) {
|
|
13601
15450
|
const lookup2 = process.platform === "win32" ? "where" : "which";
|
|
13602
15451
|
try {
|
|
13603
|
-
(0,
|
|
15452
|
+
(0, import_child_process7.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
|
|
13604
15453
|
return true;
|
|
13605
15454
|
} catch {
|
|
13606
15455
|
return false;
|
|
@@ -13630,21 +15479,32 @@ async function setup(serverUrl) {
|
|
|
13630
15479
|
);
|
|
13631
15480
|
process.exit(1);
|
|
13632
15481
|
}
|
|
13633
|
-
const
|
|
15482
|
+
const wantedHash = import_crypto2.default.createHash("sha256").update(import_fs5.default.readFileSync(dockerfilePath)).digest("hex");
|
|
15483
|
+
const existingHash = run(
|
|
13634
15484
|
"docker",
|
|
13635
|
-
[
|
|
15485
|
+
[
|
|
15486
|
+
"image",
|
|
15487
|
+
"inspect",
|
|
15488
|
+
"factiii-claude",
|
|
15489
|
+
"--format",
|
|
15490
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
|
|
15491
|
+
],
|
|
13636
15492
|
{ silent: true }
|
|
13637
|
-
);
|
|
13638
|
-
if (
|
|
13639
|
-
console.log(" Image
|
|
15493
|
+
).trim();
|
|
15494
|
+
if (existingHash === wantedHash) {
|
|
15495
|
+
console.log(" Image is up to date.");
|
|
13640
15496
|
} else {
|
|
13641
|
-
console.log(
|
|
15497
|
+
console.log(
|
|
15498
|
+
existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
|
|
15499
|
+
);
|
|
13642
15500
|
run("docker", [
|
|
13643
15501
|
"build",
|
|
13644
15502
|
"-t",
|
|
13645
15503
|
"factiii-claude",
|
|
13646
15504
|
"-f",
|
|
13647
15505
|
dockerfilePath,
|
|
15506
|
+
"--label",
|
|
15507
|
+
`${DOCKERFILE_LABEL2}=${wantedHash}`,
|
|
13648
15508
|
"."
|
|
13649
15509
|
]);
|
|
13650
15510
|
console.log(" Image built.");
|
|
@@ -13658,13 +15518,14 @@ async function setup(serverUrl) {
|
|
|
13658
15518
|
console.log("Start the runner with: factiii-runner start\n");
|
|
13659
15519
|
}
|
|
13660
15520
|
function findDockerfile() {
|
|
13661
|
-
const p =
|
|
13662
|
-
return
|
|
15521
|
+
const p = import_path6.default.join(__dirname, "..", "Dockerfile.claude");
|
|
15522
|
+
return import_fs5.default.existsSync(p) ? p : null;
|
|
13663
15523
|
}
|
|
13664
15524
|
|
|
13665
15525
|
// src/cli.ts
|
|
15526
|
+
import_node_dns.default.setDefaultResultOrder("ipv4first");
|
|
13666
15527
|
var program2 = new Command();
|
|
13667
|
-
program2.name("factiii-runner").description("Factiii Runner
|
|
15528
|
+
program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version("0.0.1");
|
|
13668
15529
|
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
15530
|
try {
|
|
13670
15531
|
await setup(opts.server);
|