@factiii/runner 0.9.7 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +2749 -825
  2. package/package.json +1 -1
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 path16 = require("node:path");
967
- var fs13 = require("node:fs");
966
+ var path17 = require("node:path");
967
+ var fs14 = 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 = path16.resolve(baseDir, baseName);
1900
- if (fs13.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path16.extname(baseName))) return void 0;
1899
+ const localBin = path17.resolve(baseDir, baseName);
1900
+ if (fs14.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path17.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs13.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs14.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 = fs13.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs14.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path16.resolve(
1920
- path16.dirname(resolvedScriptPath),
1919
+ executableDir = path17.resolve(
1920
+ path17.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 = path16.basename(
1927
+ const legacyName = path17.basename(
1928
1928
  this._scriptPath,
1929
- path16.extname(this._scriptPath)
1929
+ path17.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(path16.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path17.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 = path16.basename(filename, path16.extname(filename));
2780
+ this._name = path17.basename(filename, path17.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(path17) {
2795
- if (path17 === void 0) return this._executableDir;
2796
- this._executableDir = path17;
2794
+ executableDir(path18) {
2795
+ if (path18 === void 0) return this._executableDir;
2796
+ this._executableDir = path18;
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 fs13 = require("fs");
3029
+ var fs14 = require("fs");
3030
3030
  var Url = require("url");
3031
- var spawn4 = require("child_process").spawn;
3031
+ var spawn5 = 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
- fs13.readFile(unescape(url2.pathname), function(error, data2) {
3187
+ fs14.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 = fs13.readFileSync(unescape(url2.pathname));
3199
+ this.response = fs14.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
- fs13.writeFileSync(syncFile, "", "utf8");
3325
+ fs14.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 = spawn4(process.argv[0], ["-e", execString]);
3327
+ var syncProc = spawn5(process.argv[0], ["-e", execString]);
3328
3328
  var statusText;
3329
- while (fs13.existsSync(syncFile)) {
3329
+ while (fs14.existsSync(syncFile)) {
3330
3330
  }
3331
- self.responseText = fs13.readFileSync(contentFile, "utf8");
3331
+ self.responseText = fs14.readFileSync(contentFile, "utf8");
3332
3332
  syncProc.stdin.end();
3333
- fs13.unlinkSync(contentFile);
3333
+ fs14.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 os5 = require("os");
3987
+ var os6 = 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 = os5.release().split(".");
4025
+ const osRelease = os6.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
  }
@@ -8428,8 +8428,8 @@ function getErrorMap() {
8428
8428
 
8429
8429
  // ../../node_modules/zod/v3/helpers/parseUtil.js
8430
8430
  var makeIssue = (params) => {
8431
- const { data, path: path16, errorMaps, issueData } = params;
8432
- const fullPath = [...path16, ...issueData.path || []];
8431
+ const { data, path: path17, errorMaps, issueData } = params;
8432
+ const fullPath = [...path17, ...issueData.path || []];
8433
8433
  const fullIssue = {
8434
8434
  ...issueData,
8435
8435
  path: fullPath
@@ -8545,11 +8545,11 @@ var errorUtil;
8545
8545
 
8546
8546
  // ../../node_modules/zod/v3/types.js
8547
8547
  var ParseInputLazyPath = class {
8548
- constructor(parent, value2, path16, key) {
8548
+ constructor(parent, value2, path17, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path16;
8552
+ this._path = path17;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12908,7 +12908,7 @@ ${diff.slice(0, 8e3)}`,
12908
12908
  }
12909
12909
 
12910
12910
  // ../../shared/all/helpers/board-agent-core/engine.ts
12911
- var import_path8 = require("path");
12911
+ var import_path9 = require("path");
12912
12912
 
12913
12913
  // ../../shared/all/domains/electron.ts
12914
12914
  var SETUP_LOG_PREFIX = "log ";
@@ -12922,6 +12922,7 @@ function hostSpacePaths(spaceDir) {
12922
12922
  repo: import_path.default.join(spaceDir, "repo"),
12923
12923
  worktrees: import_path.default.join(spaceDir, "worktrees"),
12924
12924
  workspace: import_path.default.join(spaceDir, "workspace"),
12925
+ skills: import_path.default.join(spaceDir, "skills"),
12925
12926
  state: import_path.default.join(spaceDir, "state"),
12926
12927
  backups: import_path.default.join(spaceDir, "backups"),
12927
12928
  builds: import_path.default.join(spaceDir, "builds"),
@@ -12951,9 +12952,23 @@ function buildCloneUrl(repoUrl, githubToken) {
12951
12952
  async function gitInDir(target, workdir, ...args) {
12952
12953
  return target.run(["git", "-C", workdir, ...args]);
12953
12954
  }
12954
- var WORK_BRANCH_PREFIX = "factiii/card-";
12955
- function cardWorkBranch(workName) {
12956
- return `${WORK_BRANCH_PREFIX}${workName.replace(/[^a-zA-Z0-9._-]/g, "-")}`;
12955
+ var WORK_BRANCH_PREFIXES = {
12956
+ card: "card/",
12957
+ terminal: "terminal/"
12958
+ };
12959
+ var LEGACY_WORK_BRANCH_PREFIX = "factiii/card-";
12960
+ var ALL_WORK_BRANCH_PREFIXES = [
12961
+ ...Object.values(WORK_BRANCH_PREFIXES),
12962
+ LEGACY_WORK_BRANCH_PREFIX
12963
+ ].sort((a, b) => b.length - a.length);
12964
+ function workBranch(workName, kind) {
12965
+ return `${WORK_BRANCH_PREFIXES[kind]}${workName.replace(/[^a-zA-Z0-9._-]/g, "-")}`;
12966
+ }
12967
+ function workNameFromBranch(branch) {
12968
+ for (const prefix of ALL_WORK_BRANCH_PREFIXES) {
12969
+ if (branch.startsWith(prefix)) return branch.slice(prefix.length);
12970
+ }
12971
+ return null;
12957
12972
  }
12958
12973
  async function listWorkNames(target) {
12959
12974
  const dirs = await target.sh(`ls -1 ${target.paths.worktrees} 2>/dev/null || true`).catch(() => "");
@@ -12966,10 +12981,8 @@ async function listWorkNames(target) {
12966
12981
  ).catch(() => "");
12967
12982
  const names = new Set(dirs.split("\n").map((line) => line.trim()));
12968
12983
  for (const line of branches.split("\n")) {
12969
- const name = line.trim();
12970
- if (name.startsWith(WORK_BRANCH_PREFIX)) {
12971
- names.add(name.slice(WORK_BRANCH_PREFIX.length));
12972
- }
12984
+ const workName = workNameFromBranch(line.trim());
12985
+ if (workName) names.add(workName);
12973
12986
  }
12974
12987
  names.delete("");
12975
12988
  return [...names];
@@ -13003,10 +13016,18 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13003
13016
  });
13004
13017
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13005
13018
  }
13006
- async function addWorktree(target, workName, baseBranch) {
13007
- const path16 = worktreeFor(target.paths, workName);
13008
- const branch = cardWorkBranch(workName);
13009
- await removeWorktree(target, path16).catch(() => {
13019
+ async function addWorktree(target, workName, baseBranch, kind) {
13020
+ const path17 = worktreeFor(target.paths, workName);
13021
+ const branch = workBranch(workName, kind);
13022
+ await gitInDir(
13023
+ target,
13024
+ target.paths.repo,
13025
+ "fetch",
13026
+ "origin",
13027
+ baseBranch
13028
+ ).catch(() => {
13029
+ });
13030
+ await removeWorktree(target, path17).catch(() => {
13010
13031
  });
13011
13032
  await gitInDir(
13012
13033
  target,
@@ -13015,29 +13036,45 @@ async function addWorktree(target, workName, baseBranch) {
13015
13036
  "add",
13016
13037
  "-B",
13017
13038
  branch,
13018
- path16,
13039
+ path17,
13019
13040
  `origin/${baseBranch}`
13020
13041
  );
13021
- return { path: path16, branch };
13042
+ return { path: path17, branch };
13022
13043
  }
13023
- async function removeWorktree(target, path16) {
13044
+ async function removeWorktree(target, path17) {
13024
13045
  await gitInDir(
13025
13046
  target,
13026
13047
  target.paths.repo,
13027
13048
  "worktree",
13028
13049
  "remove",
13029
13050
  "--force",
13030
- path16
13051
+ path17
13031
13052
  );
13032
13053
  }
13054
+ async function removeWorktreeAndBranch(target, path17) {
13055
+ const branch = await gitInDir(
13056
+ target,
13057
+ path17,
13058
+ "rev-parse",
13059
+ "--abbrev-ref",
13060
+ "HEAD"
13061
+ ).then((out) => out.trim()).catch(() => "");
13062
+ await removeWorktree(target, path17);
13063
+ if (branch && branch !== "HEAD") {
13064
+ await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13065
+ () => {
13066
+ }
13067
+ );
13068
+ }
13069
+ }
13033
13070
  async function pruneWorktrees(target) {
13034
13071
  await gitInDir(target, target.paths.repo, "worktree", "prune").catch(
13035
13072
  () => {
13036
13073
  }
13037
13074
  );
13038
13075
  }
13039
- function isWorktreePath(target, path16) {
13040
- return path16.startsWith(`${target.paths.worktrees}/`);
13076
+ function isWorktreePath(target, path17) {
13077
+ return path17.startsWith(`${target.paths.worktrees}/`);
13041
13078
  }
13042
13079
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13043
13080
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13052,6 +13089,147 @@ async function extractDiff(target, workdir = target.paths.workspace) {
13052
13089
  changedFiles: changedFilesRaw ? changedFilesRaw.split("\n") : []
13053
13090
  };
13054
13091
  }
13092
+ var LOG_FORMAT = "%H%x1f%h%x1f%an%x1f%aI%x1f%ar%x1f%s";
13093
+ var LOG_FIELD_SEP = "";
13094
+ function parseStatus(raw) {
13095
+ const staged = [];
13096
+ const unstaged = [];
13097
+ for (const line of raw.split("\n")) {
13098
+ if (line.length < 4) continue;
13099
+ const x = line[0];
13100
+ const y = line[1];
13101
+ const rest = line.slice(3);
13102
+ const arrow = rest.indexOf(" -> ");
13103
+ const path17 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
+ if (x === "?" || y === "?") {
13105
+ unstaged.push({ path: path17, code: "A" });
13106
+ continue;
13107
+ }
13108
+ if (x !== " ") staged.push({ path: path17, code: x });
13109
+ if (y !== " ") unstaged.push({ path: path17, code: y });
13110
+ }
13111
+ return { staged, unstaged };
13112
+ }
13113
+ function parseLog(raw, localShas) {
13114
+ const commits = [];
13115
+ for (const line of raw.split("\n")) {
13116
+ if (!line.trim()) continue;
13117
+ const [sha, shortSha, author, date, relative, ...subject] = line.split(LOG_FIELD_SEP);
13118
+ if (!sha || !shortSha) continue;
13119
+ commits.push({
13120
+ sha,
13121
+ shortSha,
13122
+ author: author ?? "",
13123
+ date: date ?? "",
13124
+ relative: relative ?? "",
13125
+ // A subject holding the separator would split into extra fields; joining
13126
+ // the tail back puts it together again.
13127
+ subject: subject.join(LOG_FIELD_SEP) || "(no commit message)",
13128
+ local: localShas.has(sha)
13129
+ });
13130
+ }
13131
+ return commits;
13132
+ }
13133
+ async function readGitPanel(target, workdir, logLimit = 40) {
13134
+ const git = (...args) => gitInDir(target, workdir, ...args).catch(() => "");
13135
+ await git("add", "--intent-to-add", ".");
13136
+ const { staged, unstaged } = parseStatus(
13137
+ await git(
13138
+ "--no-optional-locks",
13139
+ "-c",
13140
+ "core.quotePath=false",
13141
+ "status",
13142
+ "--porcelain=v1",
13143
+ "--untracked-files=all"
13144
+ )
13145
+ );
13146
+ const stagedDiff = await git("diff", "--cached", "--no-color");
13147
+ const unstagedDiff = await git("diff", "--no-color");
13148
+ const diff = await git("diff", "HEAD", "--no-color");
13149
+ const localShas = new Set(
13150
+ (await git(
13151
+ "rev-list",
13152
+ "-n",
13153
+ String(logLimit),
13154
+ "HEAD",
13155
+ "--not",
13156
+ "--remotes"
13157
+ )).split("\n").map((sha) => sha.trim()).filter(Boolean)
13158
+ );
13159
+ const commits = parseLog(
13160
+ await git("log", "-n", String(logLimit), `--format=${LOG_FORMAT}`),
13161
+ localShas
13162
+ );
13163
+ const branch = (await git("rev-parse", "--abbrev-ref", "HEAD")).trim();
13164
+ return {
13165
+ branch,
13166
+ staged: { files: staged, diff: stagedDiff },
13167
+ unstaged: { files: unstaged, diff: unstagedDiff },
13168
+ commits,
13169
+ diff,
13170
+ // The union of both sides, which is what `git diff HEAD --name-only`
13171
+ // reports - one call cheaper.
13172
+ changedFiles: [
13173
+ ...new Set([...staged, ...unstaged].map((file) => file.path))
13174
+ ]
13175
+ };
13176
+ }
13177
+ function toRepoRelative(paths) {
13178
+ const clean = [];
13179
+ for (const raw of paths) {
13180
+ const rel = raw.trim().replace(/^\/+/, "");
13181
+ if (!rel || rel === ".") continue;
13182
+ if (rel.split("/").includes("..")) continue;
13183
+ clean.push(rel);
13184
+ }
13185
+ return clean;
13186
+ }
13187
+ function stageTargets(paths) {
13188
+ if (paths.length === 0) return ["."];
13189
+ const rel = toRepoRelative(paths);
13190
+ if (rel.length === 0) throw new Error("No path in that request was usable.");
13191
+ return rel;
13192
+ }
13193
+ async function stagePaths(target, workdir, paths) {
13194
+ await gitInDir(target, workdir, "add", "--", ...stageTargets(paths));
13195
+ }
13196
+ async function unstagePaths(target, workdir, paths) {
13197
+ const targets = stageTargets(paths);
13198
+ try {
13199
+ await gitInDir(target, workdir, "restore", "--staged", "--", ...targets);
13200
+ } catch {
13201
+ await gitInDir(
13202
+ target,
13203
+ workdir,
13204
+ "rm",
13205
+ "--cached",
13206
+ "-r",
13207
+ "--quiet",
13208
+ "--",
13209
+ ...targets
13210
+ );
13211
+ }
13212
+ }
13213
+ async function readCommitDiff(target, workdir, sha) {
13214
+ if (!/^[0-9a-f]{4,40}$/i.test(sha)) {
13215
+ throw new Error(`Not a commit id: ${sha}`);
13216
+ }
13217
+ const show = (...args) => gitInDir(
13218
+ target,
13219
+ workdir,
13220
+ "show",
13221
+ "--first-parent",
13222
+ "-m",
13223
+ ...args,
13224
+ sha
13225
+ ).catch(() => "");
13226
+ const diff = await show("--no-color", "--format=", "--patch");
13227
+ const names = (await show("--format=", "--name-only")).trim();
13228
+ return {
13229
+ diff,
13230
+ changedFiles: names ? names.split("\n").filter(Boolean) : []
13231
+ };
13232
+ }
13055
13233
  async function listRemoteBranches(target, workdir = target.paths.workspace) {
13056
13234
  try {
13057
13235
  const stdout = await gitInDir(
@@ -13218,7 +13396,8 @@ async function putSessionCard(s, target, prompt2 = "") {
13218
13396
  }
13219
13397
  async function teardownSession(s, target) {
13220
13398
  if (isWorktreeSession(s, target)) {
13221
- await removeWorktree(target, s.workspacePath).catch(() => {
13399
+ const drop = s.mode === "bare" ? removeWorktreeAndBranch : removeWorktree;
13400
+ await drop(target, s.workspacePath).catch(() => {
13222
13401
  });
13223
13402
  }
13224
13403
  await deleteCard(target, s.postId).catch(() => {
@@ -13665,116 +13844,1789 @@ function createEventSink(send, spaceSlug, notify) {
13665
13844
 
13666
13845
  // ../../shared/all/helpers/card-ai/frpc.ts
13667
13846
  var import_fs2 = require("fs");
13668
- var import_promises2 = __toESM(require("fs/promises"));
13669
- var import_path4 = __toESM(require("path"));
13847
+ var import_promises4 = __toESM(require("fs/promises"));
13848
+ var import_path6 = __toESM(require("path"));
13670
13849
 
13671
- // ../../shared/all/helpers/card-ai/scripts.ts
13672
- function frpcExposeScript(p) {
13673
- return `#!/bin/sh
13674
- set -e
13675
- set -a; . '${p.envFile}' 2>/dev/null || true; set +a
13676
- mkdir -p '${p.runDir}'
13677
- PORT="$1"
13678
- NAME="$2"
13679
- if [ -z "$PORT" ] || [ -z "$NAME" ]; then
13680
- echo "usage: factiii-expose <port> <name>" >&2
13681
- echo " e.g. factiii-expose 3000 app" >&2
13682
- exit 1
13683
- fi
13684
- case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
13685
- PROXY="\${FACTIII_TASK_ID}-\${NAME}"
13686
- DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
13687
- TOML="${p.runDir}/\${NAME}.toml"
13688
- cat > "$TOML" <<EOF
13689
- serverAddr = "\${FACTIII_FRP_SERVER}"
13690
- serverPort = \${FACTIII_FRP_PORT}
13691
- transport.protocol = "tcp"
13850
+ // ../../shared/all/helpers/board-agent-core/host-skills.ts
13851
+ var import_promises3 = __toESM(require("fs/promises"));
13852
+ var import_os2 = __toESM(require("os"));
13853
+ var import_path5 = __toESM(require("path"));
13692
13854
 
13693
- [metadatas]
13694
- factiii_token = "\${FACTIII_FRP_TOKEN}"
13855
+ // ../../shared/all/domains/posts.ts
13856
+ var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
13857
+ postDataSort2["DAILY"] = "DAILY";
13858
+ postDataSort2["WEEKLY"] = "WEEKLY";
13859
+ postDataSort2["MONTHLY"] = "MONTHLY";
13860
+ return postDataSort2;
13861
+ })(postDataSort || {});
13695
13862
 
13696
- [[proxies]]
13697
- name = "\${PROXY}"
13698
- type = "http"
13699
- localPort = \${PORT}
13700
- subdomain = "\${PROXY}"
13701
- EOF
13702
- # nohup, not setsid: setsid does not exist on macOS.
13703
- nohup frpc -c "$TOML" > "${p.runDir}/\${NAME}.log" 2>&1 < /dev/null &
13704
- echo $! > "${p.runDir}/\${NAME}.pid"
13705
- echo "http://\${DOMAIN}"
13706
- `;
13707
- }
13708
- function frpcUnexposeScript(p) {
13709
- return `#!/bin/sh
13710
- NAME="$1"
13711
- if [ -z "$NAME" ]; then
13712
- echo "usage: factiii-unexpose <name>" >&2
13713
- exit 1
13714
- fi
13715
- PID_FILE="${p.runDir}/\${NAME}.pid"
13716
- if [ -f "$PID_FILE" ]; then
13717
- kill "$(cat "$PID_FILE")" 2>/dev/null || true
13718
- rm -f "$PID_FILE" "${p.runDir}/\${NAME}.toml" "${p.runDir}/\${NAME}.log"
13719
- echo "stopped \${NAME}"
13720
- else
13721
- echo "no tunnel named \${NAME}" >&2
13722
- exit 1
13723
- fi
13724
- `;
13725
- }
13863
+ // ../../shared/all/domains/user.ts
13864
+ var USER_TAG_VALUES = [
13865
+ "HUMAN",
13866
+ "BOT",
13867
+ "GOVERNMENT",
13868
+ "ACADEMIA",
13869
+ "BUSINESS",
13870
+ "NEW",
13871
+ "WAITLIST",
13872
+ "VERIFIED"
13873
+ ];
13874
+ var detailsCookieSchema = external_exports.object({
13875
+ userId: external_exports.number(),
13876
+ updatedAt: external_exports.string(),
13877
+ username: external_exports.string(),
13878
+ name: external_exports.string().nullable(),
13879
+ email: external_exports.string().nullable(),
13880
+ tag: external_exports.enum(USER_TAG_VALUES),
13881
+ avatarKey: external_exports.string().nullable(),
13882
+ robohash: external_exports.string().nullable(),
13883
+ hasPassword: external_exports.boolean(),
13884
+ // Defaulted so a client cookie minted before these fields existed still parses
13885
+ // (the server re-issues it with real values on the next refresh) instead of
13886
+ // failing the whole payload and flashing the user as logged out.
13887
+ hasPasskey: external_exports.boolean().default(false),
13888
+ twoFaEnabled: external_exports.boolean(),
13889
+ oauthProviders: external_exports.array(external_exports.enum(["GOOGLE", "APPLE"])).default([]),
13890
+ // Defaulted, not just nullable: a required-but-missing key is what breaks old
13891
+ // clients, so this stays parseable once the server stops sending it.
13892
+ oauthProvider: external_exports.enum(["GOOGLE", "APPLE"]).nullable().default(null),
13893
+ emailVerificationStatus: external_exports.enum(["UNVERIFIED", "PENDING", "VERIFIED"]),
13894
+ isPrivate: external_exports.boolean(),
13895
+ isIncognito: external_exports.boolean(),
13896
+ onboardingPath: external_exports.string().nullable(),
13897
+ bio: external_exports.string().nullable(),
13898
+ bannerKey: external_exports.string().nullable(),
13899
+ tronsBalance: external_exports.number(),
13900
+ verificationRequestedAt: external_exports.string().nullable(),
13901
+ deletionScheduledAt: external_exports.string().nullable()
13902
+ });
13903
+ var UserType = /* @__PURE__ */ ((UserType2) => {
13904
+ UserType2["PRIVATE"] = "PRIVATE";
13905
+ UserType2["INCOGNITO"] = "INCOGNITO";
13906
+ UserType2["PREMIUM"] = "PREMIUM";
13907
+ UserType2["PAID"] = "PAID";
13908
+ UserType2["INVITE"] = "INVITE";
13909
+ return UserType2;
13910
+ })(UserType || {});
13726
13911
 
13727
- // ../../shared/all/helpers/card-ai/frpc.ts
13728
- var FRP_SERVER_ADDR = "dev.factiii.com";
13729
- var FRP_SERVER_PORT = 7443;
13730
- var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
13731
- function frpcPaths(target) {
13732
- const { root, bin } = target.paths;
13912
+ // ../../shared/all/uploadTypes.ts
13913
+ var _EXT_TO_CONTENT_TYPE = {
13914
+ jpg: "image/jpeg",
13915
+ jpeg: "image/jpeg",
13916
+ png: "image/png",
13917
+ gif: "image/gif",
13918
+ webp: "image/webp",
13919
+ svg: "image/svg+xml",
13920
+ heic: "image/heic",
13921
+ heif: "image/heif",
13922
+ mp4: "video/mp4",
13923
+ mov: "video/quicktime",
13924
+ mpeg: "video/mpeg",
13925
+ pdf: "application/pdf",
13926
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
13927
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
13928
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
13929
+ };
13930
+ var EXT_TO_CONTENT_TYPE = _EXT_TO_CONTENT_TYPE;
13931
+ var ALLOWED_CONTENT_TYPES = [
13932
+ ...new Set(Object.values(EXT_TO_CONTENT_TYPE))
13933
+ ];
13934
+ var ALLOWED_CONTENT_TYPES_TUPLE = ALLOWED_CONTENT_TYPES;
13935
+ var CONTENT_TYPE_TO_EXT = Object.fromEntries(
13936
+ Object.entries(EXT_TO_CONTENT_TYPE).map(([ext, mime]) => [mime, ext])
13937
+ );
13938
+ var DROPZONE_ACCEPT = Object.entries(
13939
+ EXT_TO_CONTENT_TYPE
13940
+ ).reduce((acc, [ext, mime]) => {
13941
+ (acc[mime] ??= []).push(`.${ext}`);
13942
+ return acc;
13943
+ }, {});
13944
+ var FILE_INPUT_ACCEPT = ALLOWED_CONTENT_TYPES.join(",");
13945
+
13946
+ // ../../shared/all/validators.ts
13947
+ var usernameValidationRegex = /^[a-zA-Z0-9_]+$/;
13948
+ var signupSchema = external_exports.object({
13949
+ username: external_exports.string().min(1).regex(usernameValidationRegex, {
13950
+ message: "Username can only contain letters, numbers, and underscores"
13951
+ }),
13952
+ email: external_exports.string().email(),
13953
+ password: external_exports.string().min(8, { message: "Password must contain at least 8 characters" }),
13954
+ types: external_exports.array(external_exports.nativeEnum(UserType)),
13955
+ referrerUserId: external_exports.number().optional(),
13956
+ instanceId: external_exports.number()
13957
+ });
13958
+ var usernameSchema = external_exports.string().min(1).regex(usernameValidationRegex, {
13959
+ message: "Username can only contain letters, numbers, and underscores"
13960
+ });
13961
+ var baseRegisterSchema = external_exports.object({
13962
+ username: usernameSchema,
13963
+ types: external_exports.array(external_exports.nativeEnum(UserType)).default([]),
13964
+ referrerUserId: external_exports.number().optional(),
13965
+ instanceId: external_exports.number(),
13966
+ platform: external_exports.enum(["web", "mobile"]).optional()
13967
+ });
13968
+ var passwordRegisterSchema = baseRegisterSchema.extend({
13969
+ password: external_exports.string().min(8, { message: "Password must contain at least 8 characters" }),
13970
+ recaptchaToken: external_exports.string().optional()
13971
+ });
13972
+ var createSourceSchema = external_exports.object({
13973
+ name: external_exports.string().min(1),
13974
+ description: external_exports.string().optional(),
13975
+ requirements: external_exports.array(
13976
+ external_exports.enum([
13977
+ "GOVERNMENT_SOURCE" /* GOVERNMENT_SOURCE */,
13978
+ "ENTERPRISE_SOURCE" /* ENTERPRISE_SOURCE */,
13979
+ "ANONYMOUS_SOURCE" /* ANONYMOUS_SOURCE */,
13980
+ "HUMAN_SOURCE" /* HUMAN_SOURCE */
13981
+ ])
13982
+ )
13983
+ });
13984
+ var contactFormSchema = external_exports.object({
13985
+ name: external_exports.string().min(1).max(100),
13986
+ email: external_exports.string().max(254).optional(),
13987
+ phone: external_exports.string().max(20).optional(),
13988
+ message: external_exports.string().min(1).max(2e3),
13989
+ age: external_exports.string().optional()
13990
+ }).refine((data) => !!data.email || !!data.phone, {
13991
+ message: "Either email or phone is required"
13992
+ });
13993
+ var donationSchema = external_exports.object({
13994
+ amount: external_exports.number().int().min(100).max(1e6),
13995
+ // Amount in cents (integer)
13996
+ email: external_exports.string().email()
13997
+ });
13998
+ var getUploadUrlsSchema = external_exports.object({
13999
+ contentType: external_exports.enum(ALLOWED_CONTENT_TYPES_TUPLE),
14000
+ name: external_exports.string().optional(),
14001
+ width: external_exports.number().int().positive().max(25e3).optional(),
14002
+ height: external_exports.number().int().positive().max(25e3).optional(),
14003
+ isPrivate: external_exports.boolean().optional().default(false),
14004
+ // Chat attachments pass the conversation instead of an isPrivate flag: the
14005
+ // server resolves privacy from the channel's space so the client can't
14006
+ // decide to make a PRIVATE space's files public. See uploads.getUploadUrls.
14007
+ conversationId: external_exports.string().uuid().optional(),
14008
+ isFactiii: external_exports.boolean().default(false),
14009
+ factiiiId: external_exports.number().int().positive().optional(),
14010
+ rawSha256: external_exports.string().regex(/^[a-f0-9]{64}$/i, "rawSha256 must be 64 hex chars")
14011
+ }).refine(
14012
+ (d) => d.width === void 0 && d.height === void 0 || d.width !== void 0 && d.height !== void 0,
14013
+ { message: "width and height must be provided together" }
14014
+ ).refine((d) => !d.isFactiii || d.factiiiId !== void 0, {
14015
+ message: "factiiiId is required when isFactiii is true"
14016
+ });
14017
+ var finalizeUploadSchema = external_exports.object({
14018
+ uploadId: external_exports.string(),
14019
+ isFactiii: external_exports.boolean(),
14020
+ raw: external_exports.object({
14021
+ // Bare-uuid keys; variant kind is the DB-level discriminator, no S3 prefix.
14022
+ key: external_exports.string().min(1),
14023
+ size: external_exports.number().int().positive(),
14024
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/i),
14025
+ width: external_exports.number().int().positive().optional(),
14026
+ height: external_exports.number().int().positive().optional()
14027
+ }),
14028
+ view: external_exports.object({
14029
+ key: external_exports.string().min(1),
14030
+ size: external_exports.number().int().positive(),
14031
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/i),
14032
+ width: external_exports.number().int().positive(),
14033
+ height: external_exports.number().int().positive()
14034
+ }).nullable()
14035
+ // null for non-image uploads
14036
+ });
14037
+ var variantKindSchema = external_exports.enum([
14038
+ "RAW",
14039
+ "VIEW",
14040
+ "PREMIUM_HD",
14041
+ "PREMIUM_RAW",
14042
+ "THUMBNAIL"
14043
+ ]);
14044
+ var adminUploadsGetVariantUrlSchema = external_exports.object({
14045
+ uploadId: external_exports.string(),
14046
+ kind: variantKindSchema,
14047
+ expiresIn: external_exports.number().int().positive().max(3600).default(300)
14048
+ });
14049
+ var getUploadSchema = external_exports.object({
14050
+ id: external_exports.string()
14051
+ });
14052
+ var priceSchema = external_exports.number().int().min(0, "Price must be a positive integer in cents");
14053
+ var checkPurchaseSchema = external_exports.object({
14054
+ productId: external_exports.string(),
14055
+ type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
14056
+ receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
14057
+ currency: external_exports.string().length(3).default("USD")
14058
+ });
14059
+ var creditPurchaseSchema = external_exports.object({
14060
+ orderId: external_exports.string(),
14061
+ transactionDate: external_exports.number(),
14062
+ receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
14063
+ type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
14064
+ secret: external_exports.string(),
14065
+ // Required for APPLE/GOOGLE IAP — used for server-side receipt verification
14066
+ purchaseToken: external_exports.string().optional()
14067
+ });
14068
+ var dsarFormSchema = external_exports.object({
14069
+ name: external_exports.string().trim().min(1, "Please enter your name.").max(100, "Please keep your name under 100 characters."),
14070
+ email: external_exports.string().trim().min(1, "Please enter your email address.").email("Please enter a valid email address."),
14071
+ selectedAs: external_exports.enum(["person", "agent"]),
14072
+ requestUnder: external_exports.enum(["GDPR", "CCPA", "OTHER"]),
14073
+ selectedTo: external_exports.enum(["knowledge", "delete", "optOut", "optIn", "other"]),
14074
+ details: external_exports.string().trim().min(1, "Please provide details about your request.").max(5e3, "Please keep details under 5000 characters."),
14075
+ confirmAccuracy: external_exports.boolean().refine((value2) => value2, "Please confirm your information is accurate."),
14076
+ confirmConsequences: external_exports.boolean().refine(
14077
+ (value2) => value2,
14078
+ "Please acknowledge the deletion/restriction consequences."
14079
+ ),
14080
+ confirmVerification: external_exports.boolean().refine((value2) => value2, "Please acknowledge the email verification step.")
14081
+ });
14082
+ var mapLinkSchema = external_exports.object({
14083
+ startPosition: external_exports.object({
14084
+ postId: external_exports.string().optional(),
14085
+ latitude: external_exports.number().min(-90).max(90, { message: "Latitude must be between -90 and 90" }),
14086
+ longitude: external_exports.number().min(-180).max(180, { message: "Longitude must be between -180 and 180" })
14087
+ }),
14088
+ pins: external_exports.array(
14089
+ external_exports.object({
14090
+ postId: external_exports.string().uuid({ message: "Pin ID must be a valid UUID" }),
14091
+ latitude: external_exports.number().min(-90).max(90, { message: "Pin latitude must be between -90 and 90" }),
14092
+ longitude: external_exports.number().min(-180).max(180, { message: "Pin longitude must be between -180 and 180" })
14093
+ })
14094
+ ).optional().default([])
14095
+ });
14096
+ var mapLinkPostIds = mapLinkSchema.transform((data) => {
13733
14097
  return {
13734
- binDir: bin,
13735
- binary: `${bin}/frpc`,
13736
- expose: `${bin}/factiii-expose`,
13737
- unexpose: `${bin}/factiii-unexpose`,
13738
- envFile: `${root}/.factiii-frp-env`,
13739
- runDir: `${root}/.frpc`
14098
+ startPosition: {
14099
+ postId: data.startPosition.postId,
14100
+ latitude: data.startPosition.latitude,
14101
+ longitude: data.startPosition.longitude
14102
+ },
14103
+ pins: data.pins.map((pin) => ({
14104
+ postId: pin.postId,
14105
+ latitude: pin.latitude,
14106
+ longitude: pin.longitude
14107
+ }))
14108
+ };
14109
+ });
14110
+ var mapLinkHref = mapLinkSchema.transform((data) => {
14111
+ const latParam = `latitude=${encodeURIComponent(data.startPosition.latitude)}`;
14112
+ const lonParam = `longitude=${encodeURIComponent(
14113
+ data.startPosition.longitude
14114
+ )}`;
14115
+ const pinsParam = data.pins.length > 0 ? `pins=${encodeURIComponent(JSON.stringify(data.pins))}` : "";
14116
+ return {
14117
+ startPosition: {
14118
+ postId: data.startPosition.postId,
14119
+ latitude: data.startPosition.latitude,
14120
+ longitude: data.startPosition.longitude
14121
+ },
14122
+ href: `${data.startPosition.postId ? `postId=${data.startPosition.postId}&` : ""}${latParam}&${lonParam}${pinsParam ? `&${pinsParam}` : ""}`
13740
14123
  };
14124
+ });
14125
+ var wikipediaUrlSchema = external_exports.string().refine(
14126
+ (url2) => {
14127
+ try {
14128
+ const parsedUrl = new URL(url2);
14129
+ return parsedUrl.hostname.endsWith(".wikipedia.org") && parsedUrl.protocol === "https:" && parsedUrl.pathname.startsWith("/wiki/") && parsedUrl.pathname.length > 6;
14130
+ } catch {
14131
+ return false;
14132
+ }
14133
+ },
14134
+ {
14135
+ message: "Invalid Wikipedia URL. Must be a valid HTTPS URL from wikipedia.org with /wiki/ path and article title"
14136
+ }
14137
+ );
14138
+ var localPreferenceSchema = external_exports.object({
14139
+ postFilterType: external_exports.enum(["trending", "new", "popular"]),
14140
+ postSortBy: external_exports.nativeEnum(postDataSort),
14141
+ filterBot: external_exports.boolean(),
14142
+ filterNew: external_exports.boolean(),
14143
+ showBoards: external_exports.boolean(),
14144
+ displayMode: external_exports.enum(["data", "posts", "sortByFactiii", "mediaSearch"]),
14145
+ postModeType: external_exports.enum(["home", "explore"]),
14146
+ muteVideo: external_exports.boolean(),
14147
+ theme: external_exports.enum(["dark", "light"]),
14148
+ selectedFactiiis: external_exports.array(external_exports.number()),
14149
+ dataFilter: external_exports.string(),
14150
+ mediaSearch: external_exports.string()
14151
+ });
14152
+ var localPreferenceUrlParamsSchema = external_exports.object({
14153
+ postFilterType: external_exports.enum(["trending", "new", "popular"]).optional(),
14154
+ postSortBy: external_exports.nativeEnum(postDataSort).optional(),
14155
+ filterBot: external_exports.preprocess((val) => val === "true", external_exports.boolean()).optional(),
14156
+ showBoards: external_exports.preprocess((val) => val === "true", external_exports.boolean()).optional(),
14157
+ displayMode: external_exports.enum(["data", "posts", "sortByFactiii", "mediaSearch"]).optional(),
14158
+ postModeType: external_exports.enum(["home", "explore"]).optional(),
14159
+ selectedFactiiis: external_exports.preprocess((val) => {
14160
+ if (!val || typeof val !== "string") return [];
14161
+ return val.split(",").map(Number).filter((id) => !isNaN(id));
14162
+ }, external_exports.array(external_exports.number())).optional()
14163
+ });
14164
+ var productSchema = external_exports.object({
14165
+ id: external_exports.number(),
14166
+ title: external_exports.string(),
14167
+ description: external_exports.string().nullable(),
14168
+ price: external_exports.number(),
14169
+ createdAt: external_exports.date(),
14170
+ active: external_exports.boolean(),
14171
+ originalInventory: external_exports.number().optional().nullable().transform((val) => val === void 0 ? null : val),
14172
+ inventory: external_exports.number(),
14173
+ discount: external_exports.number(),
14174
+ type: external_exports.enum(["TRON", "MONTHLY_SUBSCRIPTION", "FOUNDERS_TOKEN", "DONATION"]),
14175
+ appStoreProductId: external_exports.string().nullable(),
14176
+ playStoreProductId: external_exports.string().nullable(),
14177
+ stripePriceId: external_exports.string().nullable(),
14178
+ spaceSlug: external_exports.string(),
14179
+ images: external_exports.array(external_exports.string()),
14180
+ // Trons granted to the buyer on purchase + premium days granted. Only meaningful
14181
+ // for TRON and FOUNDERS_TOKEN; other product types ignore these. Optional so the
14182
+ // admin form can omit them when editing subscription/donation rows.
14183
+ tronQuantity: external_exports.number().int().min(0).optional(),
14184
+ tronPremiumDays: external_exports.number().int().min(0).optional()
14185
+ });
14186
+ var adminTransferSpaceOwnershipSchema = external_exports.object({
14187
+ spaceId: external_exports.number(),
14188
+ newOwnerUserId: external_exports.number()
14189
+ });
14190
+ var baseSchema = {
14191
+ cursor: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
14192
+ type: external_exports.enum(["new", "trending", "popular", "board"]).optional(),
14193
+ filterBots: external_exports.boolean().optional(),
14194
+ // Omitted = show; hiding new accounts is opt-in via the quick filter.
14195
+ filterNewAccounts: external_exports.boolean().optional(),
14196
+ showBoards: external_exports.boolean().optional(),
14197
+ factiiiIds: external_exports.number().array().optional(),
14198
+ strictFactiiiIds: external_exports.number().array().optional(),
14199
+ //used for getting posts by relatedfactiiicountgroup
14200
+ dataFilter: external_exports.string().optional(),
14201
+ start: external_exports.date().optional(),
14202
+ end: external_exports.date().optional()
14203
+ };
14204
+ var postFeedInputSchema = external_exports.discriminatedUnion("source", [
14205
+ external_exports.object({
14206
+ source: external_exports.literal("home"),
14207
+ ...baseSchema
14208
+ }),
14209
+ external_exports.object({
14210
+ source: external_exports.literal("explore"),
14211
+ ...baseSchema
14212
+ }),
14213
+ external_exports.object({
14214
+ source: external_exports.literal("user"),
14215
+ username: external_exports.string(),
14216
+ ...baseSchema
14217
+ }),
14218
+ external_exports.object({
14219
+ source: external_exports.literal("space"),
14220
+ spaceSlug: external_exports.string(),
14221
+ spaceRenderType: external_exports.enum(["USER_FACING", "SPACE_FACING", "BOTH"]).optional(),
14222
+ ...baseSchema
14223
+ }),
14224
+ external_exports.object({
14225
+ source: external_exports.literal("saved"),
14226
+ ...baseSchema
14227
+ }),
14228
+ external_exports.object({
14229
+ source: external_exports.literal("factiiis"),
14230
+ ...baseSchema
14231
+ }),
14232
+ // Every anonymous post, author masked. Backs /user/Anonymous%20User, which is
14233
+ // where anonymous posts' author links point.
14234
+ external_exports.object({
14235
+ source: external_exports.literal("anonymous"),
14236
+ ...baseSchema
14237
+ }),
14238
+ external_exports.object({
14239
+ source: external_exports.literal("search"),
14240
+ query: external_exports.string().optional(),
14241
+ spaceRenderType: external_exports.enum(["USER_FACING", "SPACE_FACING", "BOTH"]).optional(),
14242
+ userIds: external_exports.number().array().optional(),
14243
+ spaceIds: external_exports.number().array().optional(),
14244
+ ...baseSchema
14245
+ })
14246
+ ]);
14247
+ var donateTronsSchema = external_exports.object({
14248
+ amount: external_exports.number().int().positive()
14249
+ });
14250
+ var communityPoolModeSchema = external_exports.enum(["OFF", "WHITELIST", "OPEN"]);
14251
+ var communityPoolConfigSchema = external_exports.object({
14252
+ mode: communityPoolModeSchema,
14253
+ userCapPer4h: external_exports.number().int().min(0),
14254
+ poolCapPer4h: external_exports.number().int().min(0)
14255
+ });
14256
+ var crossPostPlatformEnum = external_exports.enum([
14257
+ "TIKTOK",
14258
+ "YOUTUBE",
14259
+ "INSTAGRAM",
14260
+ "FACEBOOK",
14261
+ "TWITTER",
14262
+ "LINKEDIN",
14263
+ "REDDIT"
14264
+ ]);
14265
+ var crossPostSchema = external_exports.object({
14266
+ postId: external_exports.number().int().positive(),
14267
+ platforms: external_exports.array(crossPostPlatformEnum).min(1)
14268
+ });
14269
+ var tiktokPostOptionsSchema = external_exports.object({
14270
+ postId: external_exports.number().int().positive(),
14271
+ privacyLevel: external_exports.enum([
14272
+ "PUBLIC_TO_EVERYONE",
14273
+ "MUTUAL_FOLLOW_FRIENDS",
14274
+ "FOLLOWER_OF_CREATOR",
14275
+ "SELF_ONLY"
14276
+ ]),
14277
+ disableComment: external_exports.boolean().default(false),
14278
+ disableDuet: external_exports.boolean().default(false),
14279
+ disableStitch: external_exports.boolean().default(false),
14280
+ videoCoverTimestampMs: external_exports.number().int().min(0).optional(),
14281
+ isAiGenerated: external_exports.boolean().default(false)
14282
+ });
14283
+ var crossPostSubmitSchema = external_exports.object({
14284
+ postId: external_exports.string(),
14285
+ platforms: external_exports.array(crossPostPlatformEnum).min(1).max(5)
14286
+ });
14287
+ var apiCostUpdateSchema = external_exports.object({
14288
+ platform: crossPostPlatformEnum,
14289
+ readCostTrons: external_exports.number().int().min(0),
14290
+ pushCostTrons: external_exports.number().int().min(0),
14291
+ costSource: external_exports.enum(["manual", "api"]),
14292
+ notes: external_exports.string().max(500).optional()
14293
+ });
14294
+ var startSessionSchema = external_exports.object({
14295
+ isDaily: external_exports.boolean().default(false)
14296
+ });
14297
+ var submitAnswerSchema = external_exports.object({
14298
+ sessionId: external_exports.number().int(),
14299
+ postId: external_exports.number().int(),
14300
+ answeredFact: external_exports.boolean(),
14301
+ timeMs: external_exports.number().int().min(0).max(15e3)
14302
+ });
14303
+ var endSessionSchema = external_exports.object({
14304
+ sessionId: external_exports.number().int()
14305
+ });
14306
+ var getLeaderboardSchema = external_exports.object({
14307
+ period: external_exports.enum(["daily", "weekly", "allTime"]),
14308
+ limit: external_exports.number().int().min(1).max(50).default(50)
14309
+ });
14310
+ var getSessionResultSchema = external_exports.object({
14311
+ sessionId: external_exports.number().int()
14312
+ });
14313
+ var forgeElementName = external_exports.string().trim().min(1).max(24);
14314
+ var forgeCombineSchema = external_exports.object({
14315
+ a: forgeElementName,
14316
+ b: forgeElementName
14317
+ });
14318
+ var forgeLeaderboardSchema = external_exports.object({
14319
+ limit: external_exports.number().int().min(1).max(50).default(25)
14320
+ });
14321
+ var arcadeGameSlug = external_exports.enum([
14322
+ "circle",
14323
+ "password",
14324
+ "forge",
14325
+ "spend",
14326
+ "dilemmas",
14327
+ "clicker",
14328
+ "deep",
14329
+ "robot",
14330
+ "space"
14331
+ ]);
14332
+ var recordGameStatSchema = external_exports.object({
14333
+ game: arcadeGameSlug,
14334
+ /** Canonical sortable score for that game; direction is decided client-side. */
14335
+ best: external_exports.number().finite(),
14336
+ /** Per-game extras, kept small — this lands in a JSON column. */
14337
+ detail: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number()])).optional()
14338
+ });
14339
+ var dilemmaVoteSchema = external_exports.object({
14340
+ dilemmaId: external_exports.number().int().min(1).max(200),
14341
+ pulled: external_exports.boolean()
14342
+ });
14343
+ var SKILL_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
14344
+ var skillNameSchema = external_exports.string().min(1).max(64).regex(SKILL_NAME_REGEX, {
14345
+ message: "Use only letters, numbers, dots, dashes and underscores"
14346
+ });
14347
+ var skillFrontmatterSchema = external_exports.object({
14348
+ name: skillNameSchema,
14349
+ description: external_exports.string().min(1, { message: "Description is required" }),
14350
+ // Both CLIs accept a comma-separated string or a YAML list; normalised to
14351
+ // a list on parse and written back as a list.
14352
+ "allowed-tools": external_exports.array(external_exports.string()).optional(),
14353
+ model: external_exports.string().optional(),
14354
+ license: external_exports.string().optional(),
14355
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
14356
+ "disable-model-invocation": external_exports.literal(false).optional()
14357
+ }).strict();
14358
+ var skillAgentSchema = external_exports.enum(["claude", "codex"]);
14359
+ var skillScopeSchema = external_exports.enum(["global", "repo"]);
14360
+
14361
+ // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14362
+ var import_child_process3 = require("child_process");
14363
+ var import_promises2 = __toESM(require("fs/promises"));
14364
+ var import_path4 = __toESM(require("path"));
14365
+ var import_util5 = require("util");
14366
+ var execFileAsync2 = (0, import_util5.promisify)(import_child_process3.execFile);
14367
+ var SKILL_DIR = ".agents/skills/deploy";
14368
+ var SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
14369
+ var VARIABLES_PATH = `${SKILL_DIR}/required-variables.json`;
14370
+ var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
14371
+ var CLAUDE_SKILLS_DIR = ".claude/skills";
14372
+ var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
14373
+ async function ensureClaudeSkillLink(workspace) {
14374
+ const linkPath = import_path4.default.join(workspace, CLAUDE_LINK);
14375
+ try {
14376
+ await import_promises2.default.lstat(linkPath);
14377
+ return;
14378
+ } catch {
14379
+ }
14380
+ await import_promises2.default.mkdir(import_path4.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14381
+ await import_promises2.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
13741
14382
  }
13742
- function vendoredBinaryName() {
13743
- const cpu = process.arch === "x64" ? "amd64" : process.arch;
13744
- return `frpc-${process.platform}-${cpu}`;
14383
+ var GIT_NO_HELPER = ["-c", "credential.helper="];
14384
+ var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
14385
+ async function hostGit(cwd, ...args) {
14386
+ const { stdout } = await execFileAsync2("git", [...GIT_NO_HELPER, ...args], {
14387
+ cwd,
14388
+ env: GIT_ENV,
14389
+ maxBuffer: 16 * 1024 * 1024
14390
+ });
14391
+ return stdout;
13745
14392
  }
13746
- async function installFrpcBinary(target) {
13747
- const p = frpcPaths(target);
13748
- const binaryName = vendoredBinaryName();
13749
- const src = import_path4.default.join(__dirname, "..", "vendor", binaryName);
13750
- if (!(0, import_fs2.existsSync)(src)) {
13751
- throw new Error(
13752
- `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
14393
+ async function gitCatFileBatch(cwd, shas) {
14394
+ const out = /* @__PURE__ */ new Map();
14395
+ const wanted = [...new Set(shas.filter(Boolean))];
14396
+ if (!wanted.length) return out;
14397
+ const child = (0, import_child_process3.spawn)("git", [...GIT_NO_HELPER, "cat-file", "--batch"], {
14398
+ cwd,
14399
+ env: GIT_ENV
14400
+ });
14401
+ const chunks = [];
14402
+ child.stdout.on("data", (c) => chunks.push(c));
14403
+ const done = new Promise((resolve2) => {
14404
+ child.on("close", () => resolve2());
14405
+ child.on("error", () => resolve2());
14406
+ });
14407
+ child.stdin.end(`${wanted.join("\n")}
14408
+ `);
14409
+ await done;
14410
+ let buf = Buffer.concat(chunks);
14411
+ while (buf.length) {
14412
+ const nl = buf.indexOf(10);
14413
+ if (nl === -1) break;
14414
+ const header = buf.subarray(0, nl).toString("utf-8");
14415
+ buf = buf.subarray(nl + 1);
14416
+ const [sha, type, sizeText] = header.split(" ");
14417
+ if (type === void 0 || sizeText === void 0) continue;
14418
+ const size = Number(sizeText);
14419
+ if (!Number.isFinite(size)) break;
14420
+ out.set(sha, buf.subarray(0, size).toString("utf-8"));
14421
+ buf = buf.subarray(size + 1);
14422
+ }
14423
+ return out;
14424
+ }
14425
+ async function ensureWorkspaceClone(opts) {
14426
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14427
+ const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
14428
+ const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
14429
+ let cloned = false;
14430
+ try {
14431
+ await hostGit(workspace, "rev-parse", "--git-dir");
14432
+ await hostGit(workspace, "remote", "set-url", "origin", url2);
14433
+ } catch {
14434
+ const leftovers = await import_promises2.default.readdir(workspace).catch(() => []);
14435
+ if (leftovers.includes(".git")) {
14436
+ throw new Error(
14437
+ `${workspace} holds a git repository that cannot be read. Remove it and retry.`
14438
+ );
14439
+ }
14440
+ for (const name of leftovers) {
14441
+ await import_promises2.default.rm(import_path4.default.join(workspace, name), { recursive: true, force: true });
14442
+ }
14443
+ await execFileAsync2(
14444
+ "git",
14445
+ [...GIT_NO_HELPER, "clone", "--branch", branch, url2, workspace],
14446
+ { env: GIT_ENV }
13753
14447
  );
14448
+ cloned = true;
13754
14449
  }
13755
- const staged = await import_promises2.default.stat(p.binary).catch(() => null);
13756
- const source = await import_promises2.default.stat(src);
13757
- if (staged?.size === source.size) return;
13758
- await import_promises2.default.mkdir(p.binDir, { recursive: true });
13759
- await import_promises2.default.copyFile(src, p.binary);
13760
- await import_promises2.default.chmod(p.binary, 493);
14450
+ if (!cloned) await hostGit(workspace, "fetch", "origin", branch);
14451
+ return { workspace, branch };
13761
14452
  }
13762
- async function installFrpcHarness(params) {
13763
- const { target, taskId, runnerToken } = params;
13764
- const p = frpcPaths(target);
13765
- await installFrpcBinary(target);
13766
- const envLines = [
13767
- `FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
13768
- `FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
13769
- `FACTIII_FRP_TOKEN=${runnerToken}`,
13770
- `FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
13771
- `FACTIII_TASK_ID=${taskId}`
14453
+ async function checkDeployStatus(opts) {
14454
+ const { workspace, branch } = await ensureWorkspaceClone(opts);
14455
+ const has = async (refPath) => {
14456
+ try {
14457
+ await hostGit(workspace, "cat-file", "-e", refPath);
14458
+ return true;
14459
+ } catch {
14460
+ return false;
14461
+ }
14462
+ };
14463
+ const hasAll = async (ref) => await has(`${ref}:${SKILL_PATH}`) && await has(`${ref}:${VARIABLES_PATH}`) && await has(`${ref}:${CHECKER_PATH}`);
14464
+ let pendingBranch = "";
14465
+ if (!await hasAll(`origin/${branch}`)) {
14466
+ try {
14467
+ await hostGit(
14468
+ workspace,
14469
+ "fetch",
14470
+ "origin",
14471
+ "+refs/heads/deploy-skill*:refs/remotes/origin/deploy-skill*"
14472
+ );
14473
+ const refs = await hostGit(
14474
+ workspace,
14475
+ "for-each-ref",
14476
+ "--sort=-committerdate",
14477
+ "--format=%(refname:short)",
14478
+ "refs/remotes/origin/deploy-skill*"
14479
+ );
14480
+ for (const ref of refs.split("\n").filter(Boolean)) {
14481
+ if (await hasAll(ref)) {
14482
+ pendingBranch = ref.replace(/^origin\//, "");
14483
+ break;
14484
+ }
14485
+ }
14486
+ } catch {
14487
+ }
14488
+ }
14489
+ const hasVariables = await has(`origin/${branch}:${VARIABLES_PATH}`);
14490
+ let variables = [];
14491
+ if (hasVariables) {
14492
+ try {
14493
+ const raw = await hostGit(
14494
+ workspace,
14495
+ "cat-file",
14496
+ "-p",
14497
+ `origin/${branch}:${VARIABLES_PATH}`
14498
+ );
14499
+ variables = external_exports.array(requiredVariableSchema).parse(JSON.parse(raw));
14500
+ } catch {
14501
+ }
14502
+ }
14503
+ return {
14504
+ hasSkill: await has(`origin/${branch}:${SKILL_PATH}`),
14505
+ hasVariables,
14506
+ hasChecker: await has(`origin/${branch}:${CHECKER_PATH}`),
14507
+ branch,
14508
+ pendingBranch,
14509
+ variables
14510
+ };
14511
+ }
14512
+ var requiredVariableSchema = external_exports.object({
14513
+ key: external_exports.string().min(1),
14514
+ title: external_exports.string().catch(""),
14515
+ description: external_exports.string().catch(""),
14516
+ category: external_exports.string().catch("general"),
14517
+ sensitive: external_exports.boolean().catch(true)
14518
+ });
14519
+ function spaceEnv(spaceDir) {
14520
+ const paths = hostSpacePaths(spaceDir);
14521
+ return {
14522
+ WORKSPACE: paths.workspace,
14523
+ BACKUPS_DIR: paths.backups,
14524
+ BUILDS_DIR: paths.builds,
14525
+ // No keychain credential helper and no interactive git prompts anywhere
14526
+ // in the deploy surface (agent sessions + authorized commands): auth
14527
+ // comes from the secrets store or the tokened URL, never the keychain.
14528
+ GIT_TERMINAL_PROMPT: "0",
14529
+ GIT_CONFIG_COUNT: "1",
14530
+ GIT_CONFIG_KEY_0: "credential.helper",
14531
+ GIT_CONFIG_VALUE_0: ""
14532
+ };
14533
+ }
14534
+ async function readContract(workspace) {
14535
+ const skill = await import_promises2.default.readFile(import_path4.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14536
+ throw new Error("The creator wrote no SKILL.md.");
14537
+ });
14538
+ const checker = await import_promises2.default.readFile(import_path4.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14539
+ throw new Error("The creator wrote no check-environment.sh.");
14540
+ });
14541
+ try {
14542
+ const raw = JSON.parse(
14543
+ await import_promises2.default.readFile(import_path4.default.join(workspace, VARIABLES_PATH), "utf-8")
14544
+ );
14545
+ return {
14546
+ skill,
14547
+ checker,
14548
+ variables: external_exports.array(requiredVariableSchema).parse(raw)
14549
+ };
14550
+ } catch {
14551
+ throw new Error("The creator wrote no valid required-variables.json.");
14552
+ }
14553
+ }
14554
+ async function generateDeploySkill(opts) {
14555
+ const { instructions, sendLog } = opts;
14556
+ if (!instructions.trim()) {
14557
+ throw new Error("Describe your deployment process first.");
14558
+ }
14559
+ const status = await checkDeployStatus(opts);
14560
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14561
+ sendLog({
14562
+ type: "system",
14563
+ content: `Resetting workspace to origin/${status.branch}...`
14564
+ });
14565
+ await hostGit(
14566
+ workspace,
14567
+ "checkout",
14568
+ "-B",
14569
+ status.branch,
14570
+ `origin/${status.branch}`
14571
+ );
14572
+ await hostGit(workspace, "clean", "-fd");
14573
+ sendLog({
14574
+ type: "system",
14575
+ content: "Analyzing the repo and writing the deploy skill..."
14576
+ });
14577
+ const { result } = await spawnAgent(
14578
+ opts.provider,
14579
+ hostTarget(opts.spaceDir),
14580
+ {
14581
+ cwd: workspace,
14582
+ prompt: buildPrompt(prompts_default.deployCreate, {
14583
+ INSTRUCTIONS: instructions
14584
+ }),
14585
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14586
+ // auto: the per-action classifier blocks escalations/unrecognized-infra
14587
+ // actions - a host session with no human reviewing each command should
14588
+ // not run bypassPermissions. (Codex maps this to a workspace-write
14589
+ // sandbox; see agent.ts.)
14590
+ permissionMode: "auto",
14591
+ model: "claude-opus-4-6",
14592
+ includePartialMessages: true,
14593
+ env: spaceEnv(opts.spaceDir),
14594
+ callbacks: { onLog: (entry) => sendLog(entry) }
14595
+ }
14596
+ );
14597
+ const { sessionId } = await result;
14598
+ const draft = await readContract(workspace);
14599
+ return { ...draft, sessionId };
14600
+ }
14601
+ async function reviseDeploySkill(opts) {
14602
+ const { feedback, sendLog } = opts;
14603
+ if (!feedback.trim()) throw new Error("Corrections are empty.");
14604
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14605
+ sendLog({ type: "system", content: "Applying your corrections..." });
14606
+ const { result } = await spawnAgent(
14607
+ opts.provider,
14608
+ hostTarget(opts.spaceDir),
14609
+ {
14610
+ cwd: workspace,
14611
+ prompt: buildPrompt(prompts_default.deployCreateRevise, {
14612
+ FEEDBACK: feedback
14613
+ }),
14614
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14615
+ permissionMode: "auto",
14616
+ model: "claude-opus-4-6",
14617
+ includePartialMessages: true,
14618
+ resumeSessionId: opts.resumeSessionId || void 0,
14619
+ env: spaceEnv(opts.spaceDir),
14620
+ callbacks: { onLog: (entry) => sendLog(entry) }
14621
+ }
14622
+ );
14623
+ const { sessionId } = await result;
14624
+ const draft = await readContract(workspace);
14625
+ return { ...draft, sessionId: sessionId || opts.resumeSessionId || "" };
14626
+ }
14627
+ async function loadSkillDraft(opts) {
14628
+ const branch = opts.branch.replace(/[^\w./-]/g, "");
14629
+ if (!branch.startsWith("deploy-skill")) {
14630
+ throw new Error("Not a deploy-skill branch.");
14631
+ }
14632
+ await checkDeployStatus(opts);
14633
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14634
+ await hostGit(
14635
+ workspace,
14636
+ "fetch",
14637
+ "origin",
14638
+ `+refs/heads/${branch}:refs/remotes/origin/${branch}`
14639
+ );
14640
+ await hostGit(workspace, "checkout", "-B", branch, `origin/${branch}`);
14641
+ await hostGit(workspace, "clean", "-fd");
14642
+ return readContract(workspace);
14643
+ }
14644
+ async function approveDeploySkill(opts) {
14645
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14646
+ await readContract(workspace);
14647
+ await ensureClaudeSkillLink(workspace);
14648
+ if (opts.gitName)
14649
+ await hostGit(workspace, "config", "user.name", opts.gitName);
14650
+ if (opts.gitEmail)
14651
+ await hostGit(workspace, "config", "user.email", opts.gitEmail);
14652
+ if (opts.onBranch) {
14653
+ const dirty = await hostGit(workspace, "status", "--porcelain");
14654
+ if (dirty.trim()) {
14655
+ await hostGit(workspace, "add", "-A");
14656
+ await hostGit(workspace, "commit", "-m", "Revise deploy skill");
14657
+ await hostGit(workspace, "push", "origin", opts.onBranch);
14658
+ }
14659
+ return { branch: opts.onBranch };
14660
+ }
14661
+ let target = "deploy-skill";
14662
+ try {
14663
+ await hostGit(
14664
+ workspace,
14665
+ "ls-remote",
14666
+ "--exit-code",
14667
+ "--heads",
14668
+ "origin",
14669
+ target
14670
+ );
14671
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:]/g, "").replace("T", "-");
14672
+ target = `${target}-${stamp}`;
14673
+ } catch {
14674
+ }
14675
+ await hostGit(workspace, "checkout", "-B", target);
14676
+ await hostGit(workspace, "add", "-A");
14677
+ await hostGit(workspace, "commit", "-m", "Add deploy skill");
14678
+ await hostGit(workspace, "push", "origin", target);
14679
+ return { branch: target };
14680
+ }
14681
+ async function runEnvironmentCheck(opts) {
14682
+ const status = await checkDeployStatus(opts);
14683
+ if (!status.hasChecker) {
14684
+ throw new Error(
14685
+ "No environment checker on the default branch. Generate the deploy skill first."
14686
+ );
14687
+ }
14688
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14689
+ const script = await hostGit(
14690
+ workspace,
14691
+ "cat-file",
14692
+ "-p",
14693
+ `origin/${status.branch}:${CHECKER_PATH}`
14694
+ );
14695
+ const tmp = import_path4.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14696
+ await import_promises2.default.writeFile(tmp, script, { mode: 448 });
14697
+ let ok = true;
14698
+ let output = "";
14699
+ try {
14700
+ const scriptArg = tmp.split(import_path4.default.sep).join("/");
14701
+ const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
14702
+ cwd: workspace,
14703
+ env: { ...process.env, ...spaceEnv(opts.spaceDir) },
14704
+ timeout: 12e4
14705
+ });
14706
+ output = res.output;
14707
+ if (res.code !== 0) ok = false;
14708
+ } finally {
14709
+ await import_promises2.default.rm(tmp, { force: true });
14710
+ }
14711
+ const checks = [];
14712
+ for (const line of output.split("\n")) {
14713
+ const match = /^(OK|FAIL):\s*(.+)$/.exec(line.trim());
14714
+ if (!match) continue;
14715
+ const [, verdict, rest] = match;
14716
+ const dash = rest.indexOf(" - ");
14717
+ checks.push({
14718
+ ok: verdict === "OK",
14719
+ label: dash > 0 ? rest.slice(0, dash) : rest,
14720
+ detail: dash > 0 ? rest.slice(dash + 3) : ""
14721
+ });
14722
+ if (verdict === "FAIL") ok = false;
14723
+ }
14724
+ return { ok, checks, output: output.trim().slice(-4e3) };
14725
+ }
14726
+ async function fixEnvironment(opts) {
14727
+ const { sendLog } = opts;
14728
+ const before = await runEnvironmentCheck(opts);
14729
+ if (before.ok) return before;
14730
+ const status = await checkDeployStatus(opts);
14731
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14732
+ const script = await hostGit(
14733
+ workspace,
14734
+ "cat-file",
14735
+ "-p",
14736
+ `origin/${status.branch}:${CHECKER_PATH}`
14737
+ );
14738
+ const failed = before.checks.filter((c) => !c.ok).map((c) => `- ${c.label}${c.detail ? ` (${c.detail})` : ""}`).join("\n");
14739
+ sendLog({
14740
+ type: "system",
14741
+ content: "Setting this machine up to pass the environment check..."
14742
+ });
14743
+ const { result } = await spawnAgent(
14744
+ opts.provider,
14745
+ hostTarget(opts.spaceDir),
14746
+ {
14747
+ cwd: workspace,
14748
+ prompt: buildPrompt(prompts_default.deployEnvFix, {
14749
+ CHECKS: failed || before.output.slice(-2e3),
14750
+ SCRIPT: script
14751
+ }),
14752
+ allowedTools: "Bash,Read,Grep,Glob,WebSearch,WebFetch",
14753
+ // auto: installs are in-scope of the owner's explicit "fix environment"
14754
+ // request; the classifier still blocks anything beyond it.
14755
+ permissionMode: "auto",
14756
+ model: "claude-opus-4-6",
14757
+ includePartialMessages: true,
14758
+ env: spaceEnv(opts.spaceDir),
14759
+ callbacks: { onLog: (entry) => sendLog(entry) }
14760
+ }
14761
+ );
14762
+ await result;
14763
+ sendLog({ type: "system", content: "Re-running the environment check..." });
14764
+ return runEnvironmentCheck(opts);
14765
+ }
14766
+ async function discardDeploySkill(opts) {
14767
+ const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14768
+ await hostGit(workspace, "checkout", "--", ".").catch(() => null);
14769
+ await hostGit(workspace, "clean", "-fd");
14770
+ return { success: true };
14771
+ }
14772
+
14773
+ // ../../shared/all/helpers/board-agent-core/skill-format.ts
14774
+ var EMPTY_FRONTMATTER = { name: "", description: "" };
14775
+ function splitKey(line) {
14776
+ const at = line.indexOf(":");
14777
+ if (at < 1) return null;
14778
+ return { key: line.slice(0, at).trim(), value: line.slice(at + 1).trim() };
14779
+ }
14780
+ function stripQuotes(value2) {
14781
+ const quoted = value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'");
14782
+ return quoted && value2.length > 1 ? value2.slice(1, -1) : value2;
14783
+ }
14784
+ function parseSkill(raw) {
14785
+ const text = raw.replace(/^/, "");
14786
+ if (!text.startsWith("---")) {
14787
+ return {
14788
+ frontmatter: EMPTY_FRONTMATTER,
14789
+ body: text,
14790
+ errors: ["Must start with YAML frontmatter (---)."]
14791
+ };
14792
+ }
14793
+ const lines = text.split("\n");
14794
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
14795
+ if (end === -1) {
14796
+ return {
14797
+ frontmatter: EMPTY_FRONTMATTER,
14798
+ body: text,
14799
+ errors: ["Frontmatter is never closed (missing the second ---)."]
14800
+ };
14801
+ }
14802
+ const errors = [];
14803
+ const fields = {};
14804
+ let pendingKey = "";
14805
+ let list = [];
14806
+ let map = {};
14807
+ const flush = () => {
14808
+ if (pendingKey) {
14809
+ if (list.length) fields[pendingKey] = list;
14810
+ else if (Object.keys(map).length) fields[pendingKey] = map;
14811
+ }
14812
+ pendingKey = "";
14813
+ list = [];
14814
+ map = {};
14815
+ };
14816
+ for (const line of lines.slice(1, end)) {
14817
+ if (!line.trim() || line.trim().startsWith("#")) continue;
14818
+ const indented = /^\s/.test(line);
14819
+ if (indented && pendingKey) {
14820
+ if (line.trim().startsWith("- ")) {
14821
+ list.push(stripQuotes(line.trim().slice(2).trim()));
14822
+ } else {
14823
+ const nested = splitKey(line);
14824
+ if (nested) map[nested.key] = stripQuotes(nested.value);
14825
+ else errors.push(`Cannot read indented line: ${line.trim()}`);
14826
+ }
14827
+ continue;
14828
+ }
14829
+ if (indented) {
14830
+ errors.push(`Cannot read indented line: ${line.trim()}`);
14831
+ continue;
14832
+ }
14833
+ flush();
14834
+ const pair = splitKey(line);
14835
+ if (!pair) {
14836
+ errors.push(`Cannot read line: ${line.trim()}`);
14837
+ continue;
14838
+ }
14839
+ if (pair.value === "") {
14840
+ pendingKey = pair.key;
14841
+ continue;
14842
+ }
14843
+ fields[pair.key] = stripQuotes(pair.value);
14844
+ }
14845
+ flush();
14846
+ const tools = fields["allowed-tools"];
14847
+ if (typeof tools === "string") {
14848
+ fields["allowed-tools"] = tools.split(",").map((tool) => tool.trim()).filter(Boolean);
14849
+ }
14850
+ if (fields["disable-model-invocation"] === "true") {
14851
+ errors.push("disable-model-invocation must be false.");
14852
+ delete fields["disable-model-invocation"];
14853
+ }
14854
+ if (fields["disable-model-invocation"] === "false") {
14855
+ fields["disable-model-invocation"] = false;
14856
+ }
14857
+ const body = lines.slice(end + 1).join("\n").replace(/^\n/, "");
14858
+ const parsed = skillFrontmatterSchema.safeParse(fields);
14859
+ if (!parsed.success) {
14860
+ for (const issue of parsed.error.issues) {
14861
+ errors.push(`${issue.path.join(".") || "frontmatter"}: ${issue.message}`);
14862
+ }
14863
+ return {
14864
+ frontmatter: {
14865
+ name: typeof fields.name === "string" ? fields.name : "",
14866
+ description: typeof fields.description === "string" ? fields.description : ""
14867
+ },
14868
+ body,
14869
+ errors
14870
+ };
14871
+ }
14872
+ return { frontmatter: parsed.data, body, errors };
14873
+ }
14874
+ function resolveSkillDir(root, name) {
14875
+ if (!SKILL_NAME_REGEX.test(name) || name === "." || name === "..") {
14876
+ throw new Error(`Invalid skill name: ${name}`);
14877
+ }
14878
+ return `${root.replace(/[/\\]+$/, "")}/${name}`;
14879
+ }
14880
+
14881
+ // ../../shared/all/helpers/board-agent-core/host-skills.ts
14882
+ var AGENTS_ROOT = ".agents/skills";
14883
+ var CLAUDE_ROOT = ".claude/skills";
14884
+ var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
14885
+ function globalAgentsRoot() {
14886
+ return import_path5.default.join(import_os2.default.homedir(), ".agents", "skills");
14887
+ }
14888
+ var AGENTS_LINK_TARGET = "../.agents/skills";
14889
+ var BRANCH_PREFIX = "skill/";
14890
+ var SYMLINK_MODE = "120000";
14891
+ function summarize(raw, base) {
14892
+ const { frontmatter, errors } = parseSkill(raw);
14893
+ return { ...base, description: frontmatter.description, errors };
14894
+ }
14895
+ async function rootLinkState(root) {
14896
+ const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
14897
+ const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
14898
+ if (!stat) return "missing";
14899
+ if (!stat.isSymbolicLink()) return "blocked";
14900
+ const target = await import_promises3.default.readlink(linkPath).catch(() => "");
14901
+ return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
14902
+ }
14903
+ async function globalLinkState() {
14904
+ const states = await Promise.all(CLI_ROOTS.map(rootLinkState));
14905
+ if (states.includes("blocked")) return "blocked";
14906
+ return states.includes("missing") ? "missing" : "linked";
14907
+ }
14908
+ async function listGlobal() {
14909
+ const root = globalAgentsRoot();
14910
+ const entries = await import_promises3.default.readdir(root, { withFileTypes: true }).catch(() => []);
14911
+ const out = [];
14912
+ for (const entry of entries) {
14913
+ if (entry.isFile()) continue;
14914
+ const dir = import_path5.default.join(root, entry.name);
14915
+ const raw = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
14916
+ if (!raw) continue;
14917
+ out.push(
14918
+ summarize(raw, {
14919
+ name: entry.name,
14920
+ scope: "global",
14921
+ dir,
14922
+ status: "live",
14923
+ pendingBranch: ""
14924
+ })
14925
+ );
14926
+ }
14927
+ return out.sort((a, b) => a.name.localeCompare(b.name));
14928
+ }
14929
+ function strayRepoSkills(liveBlobs) {
14930
+ return [...liveBlobs.keys()].filter((key) => CLI_ROOTS.some((root) => key.startsWith(`${root}/`))).sort();
14931
+ }
14932
+ async function treeAt(repo, ref) {
14933
+ const raw = await hostGit(
14934
+ repo,
14935
+ "ls-tree",
14936
+ "-r",
14937
+ ref,
14938
+ "--",
14939
+ AGENTS_ROOT,
14940
+ ...CLI_ROOTS
14941
+ ).catch(() => "");
14942
+ return raw.split("\n").filter(Boolean).map((line) => {
14943
+ const [meta, filePath] = line.split(" ");
14944
+ const [mode, , sha] = meta.split(/\s+/);
14945
+ return { mode, sha, filePath };
14946
+ }).filter((entry) => Boolean(entry.filePath));
14947
+ }
14948
+ function skillBlobs(entries) {
14949
+ const blobs = /* @__PURE__ */ new Map();
14950
+ for (const entry of entries) {
14951
+ const match = /^(.+\/skills)\/([^/]+)\/SKILL\.md$/.exec(entry.filePath);
14952
+ if (match) blobs.set(`${match[1]}/${match[2]}`, entry.sha);
14953
+ }
14954
+ return blobs;
14955
+ }
14956
+ async function skillLinks(repo, entries) {
14957
+ const links = /* @__PURE__ */ new Map();
14958
+ const linkEntries = entries.filter(
14959
+ (entry) => entry.mode === SYMLINK_MODE && /^.+\/skills(\/[^/]+)?$/.test(entry.filePath)
14960
+ );
14961
+ const targets = await gitCatFileBatch(
14962
+ repo,
14963
+ linkEntries.map((entry) => entry.sha)
14964
+ );
14965
+ for (const entry of linkEntries) {
14966
+ const target = targets.get(entry.sha)?.trim();
14967
+ if (target) links.set(entry.filePath, target);
14968
+ }
14969
+ return links;
14970
+ }
14971
+ function resolveRepoSkills(liveBlobs, pending) {
14972
+ const keys = /* @__PURE__ */ new Set();
14973
+ for (const key of liveBlobs.keys()) {
14974
+ if (key.startsWith(`${AGENTS_ROOT}/`)) keys.add(key);
14975
+ }
14976
+ for (const entry of pending) {
14977
+ for (const key of entry.blobs.keys()) {
14978
+ if (key.startsWith(`${AGENTS_ROOT}/`)) keys.add(key);
14979
+ }
14980
+ }
14981
+ const out = [];
14982
+ for (const key of [...keys].sort()) {
14983
+ const name = key.slice(AGENTS_ROOT.length + 1);
14984
+ const live = liveBlobs.get(key) || "";
14985
+ const onBranch = pending.find((entry) => {
14986
+ const sha = entry.blobs.get(key);
14987
+ return Boolean(sha) && sha !== live;
14988
+ });
14989
+ const status = live ? onBranch ? "pending-edit" : "live" : "pending-new";
14990
+ out.push({
14991
+ name,
14992
+ dir: key,
14993
+ status,
14994
+ pendingBranch: status === "live" ? "" : onBranch?.branch || "",
14995
+ sha: live || onBranch?.blobs.get(key) || ""
14996
+ });
14997
+ }
14998
+ return out;
14999
+ }
15000
+ async function repoSkills(ctx) {
15001
+ const skills = resolveRepoSkills(ctx.liveBlobs, ctx.pending);
15002
+ const cache2 = /* @__PURE__ */ new Map();
15003
+ const missing = skills.map((skill) => skill.sha).filter((sha) => sha && !cache2.has(sha));
15004
+ for (const [sha, text] of await gitCatFileBatch(ctx.repo, missing)) {
15005
+ cache2.set(sha, text);
15006
+ }
15007
+ return skills.map(
15008
+ (skill) => summarize(cache2.get(skill.sha) ?? "", {
15009
+ name: skill.name,
15010
+ scope: "repo",
15011
+ dir: skill.dir,
15012
+ status: skill.status,
15013
+ pendingBranch: skill.pendingBranch
15014
+ })
15015
+ );
15016
+ }
15017
+ async function pendingBranches(repo, mainBranch) {
15018
+ const refs = await hostGit(
15019
+ repo,
15020
+ "for-each-ref",
15021
+ "--sort=-committerdate",
15022
+ "--format=%(refname:short)",
15023
+ `refs/remotes/origin/${BRANCH_PREFIX}*`
15024
+ ).catch(() => "");
15025
+ const branches = await Promise.all(
15026
+ refs.split("\n").filter(Boolean).map(async (ref) => {
15027
+ const merged = await hostGit(
15028
+ repo,
15029
+ "merge-base",
15030
+ "--is-ancestor",
15031
+ ref,
15032
+ `origin/${mainBranch}`
15033
+ ).then(
15034
+ () => true,
15035
+ () => false
15036
+ );
15037
+ if (merged) return null;
15038
+ return {
15039
+ branch: ref.replace(/^origin\//, ""),
15040
+ blobs: skillBlobs(await treeAt(repo, ref))
15041
+ };
15042
+ })
15043
+ );
15044
+ return branches.filter((entry) => entry !== null);
15045
+ }
15046
+ function requireRepo(opts) {
15047
+ if (!opts.repoUrl || !opts.githubToken) {
15048
+ throw new Error("Set the repository URL and GitHub token first.");
15049
+ }
15050
+ return {
15051
+ spaceDir: opts.spaceDir,
15052
+ repoUrl: opts.repoUrl,
15053
+ mainBranch: opts.mainBranch,
15054
+ githubToken: opts.githubToken
15055
+ };
15056
+ }
15057
+ async function ensureSkillsRepo(opts) {
15058
+ const { spaceDir, repoUrl, mainBranch, githubToken } = requireRepo(opts);
15059
+ const paths = hostSpacePaths(spaceDir);
15060
+ const branch = (mainBranch || "main").replace(/[^\w./-]/g, "");
15061
+ const url2 = buildCloneUrl(repoUrl, githubToken);
15062
+ try {
15063
+ await hostGit(paths.repo, "rev-parse", "--git-dir");
15064
+ await hostGit(paths.repo, "remote", "set-url", "origin", url2);
15065
+ } catch {
15066
+ await hostGit(paths.root, "clone", "--branch", branch, url2, paths.repo);
15067
+ }
15068
+ await hostGit(
15069
+ paths.repo,
15070
+ "fetch",
15071
+ "origin",
15072
+ branch,
15073
+ `+refs/heads/${BRANCH_PREFIX}*:refs/remotes/origin/${BRANCH_PREFIX}*`
15074
+ );
15075
+ return { repo: paths.repo, branch };
15076
+ }
15077
+ async function ensureSkillsWorktree(spaceDir, branch) {
15078
+ const { repo, skills: worktree } = hostSpacePaths(spaceDir);
15079
+ try {
15080
+ await hostGit(worktree, "rev-parse", "--git-dir");
15081
+ } catch {
15082
+ await hostGit(repo, "worktree", "prune").catch(() => {
15083
+ });
15084
+ await import_promises3.default.rm(worktree, { recursive: true, force: true });
15085
+ await hostGit(
15086
+ repo,
15087
+ "worktree",
15088
+ "add",
15089
+ "--detach",
15090
+ worktree,
15091
+ `origin/${branch}`
15092
+ );
15093
+ }
15094
+ return worktree;
15095
+ }
15096
+ async function repoContext(opts) {
15097
+ const { repo, branch } = await ensureSkillsRepo(opts);
15098
+ const liveTree = await treeAt(repo, `origin/${branch}`);
15099
+ const [links, pending] = await Promise.all([
15100
+ skillLinks(repo, liveTree),
15101
+ pendingBranches(repo, branch)
15102
+ ]);
15103
+ return {
15104
+ repo,
15105
+ branch,
15106
+ liveTree,
15107
+ liveBlobs: skillBlobs(liveTree),
15108
+ links,
15109
+ pending
15110
+ };
15111
+ }
15112
+ function findRepoSkill(ctx, name) {
15113
+ return resolveRepoSkills(ctx.liveBlobs, ctx.pending).find(
15114
+ (skill) => skill.name === name
15115
+ ) ?? null;
15116
+ }
15117
+ var workspaceQueues = /* @__PURE__ */ new Map();
15118
+ function withWorkspace(spaceDir, run2) {
15119
+ const next = (workspaceQueues.get(spaceDir) ?? Promise.resolve()).then(
15120
+ run2,
15121
+ run2
15122
+ );
15123
+ workspaceQueues.set(
15124
+ spaceDir,
15125
+ next.catch(() => {
15126
+ })
15127
+ );
15128
+ return next;
15129
+ }
15130
+ function repoLinkState(ctx) {
15131
+ const target = ctx.links.get(CLAUDE_ROOT);
15132
+ if (target) return target === AGENTS_LINK_TARGET ? "linked" : "missing";
15133
+ return [...ctx.liveBlobs.keys()].some(
15134
+ (key) => key.startsWith(`${CLAUDE_ROOT}/`)
15135
+ ) ? "blocked" : "missing";
15136
+ }
15137
+ async function listSkillsImpl(opts) {
15138
+ const [global2, globalLink] = await Promise.all([
15139
+ listGlobal(),
15140
+ globalLinkState()
15141
+ ]);
15142
+ if (!opts.repoUrl || !opts.githubToken) {
15143
+ return {
15144
+ global: global2,
15145
+ repo: [],
15146
+ globalLink,
15147
+ repoLink: "linked",
15148
+ repoBranch: "",
15149
+ repoAvailable: false
15150
+ };
15151
+ }
15152
+ const ctx = await repoContext(opts);
15153
+ const repo = await repoSkills(ctx);
15154
+ return {
15155
+ global: global2,
15156
+ repo,
15157
+ globalLink,
15158
+ repoLink: repoLinkState(ctx),
15159
+ repoBranch: ctx.branch,
15160
+ repoAvailable: true
15161
+ };
15162
+ }
15163
+ async function bundleFilesOnDisk(dir) {
15164
+ const out = [];
15165
+ const walk = async (current, prefix, depth) => {
15166
+ if (depth > 3 || out.length >= 200) return;
15167
+ const entries = await import_promises3.default.readdir(current, { withFileTypes: true }).catch(() => []);
15168
+ for (const entry of entries) {
15169
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
15170
+ if (entry.isDirectory()) {
15171
+ await walk(import_path5.default.join(current, entry.name), rel, depth + 1);
15172
+ } else if (rel !== "SKILL.md") {
15173
+ out.push(rel);
15174
+ }
15175
+ }
15176
+ };
15177
+ await walk(dir, "", 0);
15178
+ return out.sort();
15179
+ }
15180
+ async function readSkillImpl(opts) {
15181
+ if (opts.scope === "global") {
15182
+ const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15183
+ const content2 = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8");
15184
+ const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
15185
+ return {
15186
+ name: opts.name,
15187
+ scope: "global",
15188
+ content: content2,
15189
+ frontmatter: frontmatter2,
15190
+ errors: errors2,
15191
+ bundleFiles: await bundleFilesOnDisk(dir),
15192
+ status: "live",
15193
+ pendingBranch: ""
15194
+ };
15195
+ }
15196
+ const ctx = await repoContext(opts);
15197
+ const skill = findRepoSkill(ctx, opts.name);
15198
+ if (!skill) throw new Error(`No repo skill named ${opts.name}.`);
15199
+ const draft = skill.pendingBranch ? ctx.pending.find((entry) => entry.branch === skill.pendingBranch)?.blobs.get(skill.dir) : "";
15200
+ const content = await hostGit(ctx.repo, "cat-file", "-p", draft || skill.sha);
15201
+ const { frontmatter, errors } = parseSkill(content);
15202
+ const tree = draft ? await treeAt(ctx.repo, `origin/${skill.pendingBranch}`) : ctx.liveTree;
15203
+ const bundleFiles = tree.filter(
15204
+ (entry) => entry.filePath.startsWith(`${skill.dir}/`) && entry.filePath !== `${skill.dir}/SKILL.md`
15205
+ ).map((entry) => entry.filePath.slice(skill.dir.length + 1)).sort();
15206
+ return {
15207
+ name: skill.name,
15208
+ scope: "repo",
15209
+ content,
15210
+ frontmatter,
15211
+ errors,
15212
+ bundleFiles,
15213
+ status: skill.status,
15214
+ pendingBranch: skill.pendingBranch
15215
+ };
15216
+ }
15217
+ async function moveIntoAgents(from, to) {
15218
+ if (await import_promises3.default.access(to).then(
15219
+ () => true,
15220
+ () => false
15221
+ )) {
15222
+ throw new Error(
15223
+ `${import_path5.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15224
+ );
15225
+ }
15226
+ await import_promises3.default.mkdir(import_path5.default.dirname(to), { recursive: true });
15227
+ try {
15228
+ await import_promises3.default.rename(from, to);
15229
+ } catch {
15230
+ await import_promises3.default.cp(from, to, { recursive: true });
15231
+ await import_promises3.default.rm(from, { recursive: true, force: true });
15232
+ }
15233
+ }
15234
+ async function migrateGlobal() {
15235
+ const moved = [];
15236
+ for (const root of CLI_ROOTS) {
15237
+ const dir = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15238
+ const stat = await import_promises3.default.lstat(dir).catch(() => null);
15239
+ if (!stat || stat.isSymbolicLink()) continue;
15240
+ const entries = await import_promises3.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15241
+ for (const entry of entries) {
15242
+ if (entry.isFile() || entry.isSymbolicLink()) continue;
15243
+ const src = import_path5.default.join(dir, entry.name);
15244
+ if (!await import_promises3.default.access(import_path5.default.join(src, "SKILL.md")).then(
15245
+ () => true,
15246
+ () => false
15247
+ )) {
15248
+ continue;
15249
+ }
15250
+ await moveIntoAgents(src, import_path5.default.join(globalAgentsRoot(), entry.name));
15251
+ moved.push(entry.name);
15252
+ }
15253
+ await import_promises3.default.rmdir(dir).catch(() => {
15254
+ });
15255
+ }
15256
+ for (const root of CLI_ROOTS) {
15257
+ const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15258
+ const after = await import_promises3.default.lstat(linkPath).catch(() => null);
15259
+ if (after?.isSymbolicLink()) await import_promises3.default.rm(linkPath, { force: true });
15260
+ else if (after) {
15261
+ throw new Error(
15262
+ `~/${root} still has files that are not skills. Empty it, then migrate.`
15263
+ );
15264
+ }
15265
+ await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15266
+ await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15267
+ }
15268
+ return moved;
15269
+ }
15270
+ async function migrateSkillsImpl(opts) {
15271
+ if (opts.scope === "global") {
15272
+ return { branch: "", moved: await migrateGlobal() };
15273
+ }
15274
+ const ctx = await repoContext(opts);
15275
+ const strays = strayRepoSkills(ctx.liveBlobs);
15276
+ const linked = ctx.links.get(CLAUDE_ROOT);
15277
+ if (!strays.length && linked === AGENTS_LINK_TARGET) {
15278
+ return { branch: "", moved: [] };
15279
+ }
15280
+ const { branch } = ctx;
15281
+ const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
15282
+ const target = `${BRANCH_PREFIX}migrate`;
15283
+ await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
15284
+ await hostGit(workspace, "clean", "-fd");
15285
+ if (opts.gitName) {
15286
+ await hostGit(workspace, "config", "user.name", opts.gitName);
15287
+ }
15288
+ if (opts.gitEmail) {
15289
+ await hostGit(workspace, "config", "user.email", opts.gitEmail);
15290
+ }
15291
+ const moved = [];
15292
+ for (const stray of strays) {
15293
+ const name = stray.slice(stray.lastIndexOf("/") + 1);
15294
+ const dest = `${AGENTS_ROOT}/${name}`;
15295
+ if (ctx.liveBlobs.has(dest)) {
15296
+ throw new Error(
15297
+ `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
15298
+ );
15299
+ }
15300
+ await import_promises3.default.mkdir(import_path5.default.join(workspace, AGENTS_ROOT), { recursive: true });
15301
+ await hostGit(workspace, "mv", stray, dest);
15302
+ moved.push(name);
15303
+ }
15304
+ const linkPath = import_path5.default.join(workspace, CLAUDE_ROOT);
15305
+ await import_promises3.default.rm(linkPath, { recursive: true, force: true });
15306
+ await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15307
+ await hostGit(workspace, "add", "-A");
15308
+ const dirty = await hostGit(workspace, "status", "--porcelain");
15309
+ if (!dirty.trim()) return { branch: "", moved: [] };
15310
+ await hostGit(
15311
+ workspace,
15312
+ "commit",
15313
+ "-m",
15314
+ `Migrate ${moved.length} skill(s) to ${AGENTS_ROOT} and link ${CLAUDE_ROOT}`
15315
+ );
15316
+ await hostGit(workspace, "push", "origin", target);
15317
+ return { branch: target, moved };
15318
+ }
15319
+ function assertWritable(name, content) {
15320
+ const parsed = parseSkill(content);
15321
+ if (parsed.errors.length) {
15322
+ throw new Error(`Fix the skill first: ${parsed.errors.join(" ")}`);
15323
+ }
15324
+ if (parsed.frontmatter.name !== name) {
15325
+ throw new Error(
15326
+ `Frontmatter name (${parsed.frontmatter.name}) must match the skill directory (${name}).`
15327
+ );
15328
+ }
15329
+ }
15330
+ async function writeSkillFile(opts) {
15331
+ const dir = resolveSkillDir(opts.root, opts.name);
15332
+ const file = import_path5.default.join(dir, "SKILL.md");
15333
+ if (opts.keepExisting) {
15334
+ const exists = await import_promises3.default.access(file).then(
15335
+ () => true,
15336
+ () => false
15337
+ );
15338
+ if (exists) return;
15339
+ }
15340
+ await import_promises3.default.mkdir(dir, { recursive: true });
15341
+ await import_promises3.default.writeFile(file, opts.content, "utf-8");
15342
+ }
15343
+ async function writeGlobalSkill(opts) {
15344
+ assertWritable(opts.name, opts.content);
15345
+ await writeSkillFile({ root: globalAgentsRoot(), ...opts });
15346
+ }
15347
+ async function installBundledSkill(opts) {
15348
+ assertWritable(opts.name, opts.content);
15349
+ const home = opts.home ?? import_os2.default.homedir();
15350
+ const write = (root) => writeSkillFile({
15351
+ root,
15352
+ name: opts.name,
15353
+ content: opts.content,
15354
+ keepExisting: true
15355
+ });
15356
+ await write(import_path5.default.join(home, ...AGENTS_ROOT.split("/")));
15357
+ for (const root of CLI_ROOTS) {
15358
+ const linkPath = import_path5.default.join(home, ...root.split("/"));
15359
+ const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
15360
+ if (stat?.isSymbolicLink()) continue;
15361
+ if (!stat) {
15362
+ await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15363
+ await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15364
+ continue;
15365
+ }
15366
+ await write(linkPath);
15367
+ }
15368
+ }
15369
+ async function deleteGlobalSkill(opts) {
15370
+ const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15371
+ await import_promises3.default.rm(dir, { recursive: true, force: true });
15372
+ }
15373
+ async function proposeRepoSkillImpl(opts) {
15374
+ assertWritable(opts.name, opts.content);
15375
+ if (!SKILL_NAME_REGEX.test(opts.name)) {
15376
+ throw new Error(`Invalid skill name: ${opts.name}`);
15377
+ }
15378
+ const ctx = await repoContext(opts);
15379
+ const known = findRepoSkill(ctx, opts.name);
15380
+ const dir = known?.dir || `${AGENTS_ROOT}/${opts.name}`;
15381
+ const { branch } = ctx;
15382
+ const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
15383
+ const target = `${BRANCH_PREFIX}${opts.name}`;
15384
+ const existing = await hostGit(
15385
+ workspace,
15386
+ "ls-remote",
15387
+ "--heads",
15388
+ "origin",
15389
+ target
15390
+ ).catch(() => "");
15391
+ if (existing.trim()) {
15392
+ await hostGit(
15393
+ workspace,
15394
+ "fetch",
15395
+ "origin",
15396
+ `+refs/heads/${target}:refs/remotes/origin/${target}`
15397
+ );
15398
+ await hostGit(workspace, "checkout", "-B", target, `origin/${target}`);
15399
+ } else {
15400
+ await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
15401
+ }
15402
+ await hostGit(workspace, "clean", "-fd");
15403
+ if (opts.gitName) {
15404
+ await hostGit(workspace, "config", "user.name", opts.gitName);
15405
+ }
15406
+ if (opts.gitEmail) {
15407
+ await hostGit(workspace, "config", "user.email", opts.gitEmail);
15408
+ }
15409
+ await import_promises3.default.mkdir(import_path5.default.join(workspace, dir), { recursive: true });
15410
+ await import_promises3.default.writeFile(
15411
+ import_path5.default.join(workspace, dir, "SKILL.md"),
15412
+ opts.content,
15413
+ "utf-8"
15414
+ );
15415
+ const dirty = await hostGit(workspace, "status", "--porcelain");
15416
+ if (!dirty.trim()) return { branch: target };
15417
+ await hostGit(workspace, "add", "-A");
15418
+ await hostGit(
15419
+ workspace,
15420
+ "commit",
15421
+ "-m",
15422
+ `${known ? "Update" : "Add"} skill: ${opts.name}`
15423
+ );
15424
+ await hostGit(workspace, "push", "origin", target);
15425
+ return { branch: target };
15426
+ }
15427
+ function listSkills(opts) {
15428
+ return withWorkspace(opts.spaceDir, () => listSkillsImpl(opts));
15429
+ }
15430
+ function readSkill(opts) {
15431
+ return withWorkspace(opts.spaceDir, () => readSkillImpl(opts));
15432
+ }
15433
+ function migrateSkills(opts) {
15434
+ return withWorkspace(opts.spaceDir, () => migrateSkillsImpl(opts));
15435
+ }
15436
+ function proposeRepoSkill(opts) {
15437
+ return withWorkspace(opts.spaceDir, () => proposeRepoSkillImpl(opts));
15438
+ }
15439
+
15440
+ // ../../shared/all/helpers/card-ai/scripts.ts
15441
+ function frpcExposeScript(p) {
15442
+ return `#!/bin/sh
15443
+ set -e
15444
+ set -a; . '${p.envFile}' 2>/dev/null || true; set +a
15445
+ mkdir -p '${p.runDir}'
15446
+ PORT="$1"
15447
+ NAME="$2"
15448
+ if [ -z "$PORT" ] || [ -z "$NAME" ]; then
15449
+ echo "usage: factiii-expose <port> <name>" >&2
15450
+ echo " e.g. factiii-expose 3000 app" >&2
15451
+ exit 1
15452
+ fi
15453
+ case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
15454
+ PROXY="\${FACTIII_TASK_ID}-\${NAME}"
15455
+ DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
15456
+ TOML="${p.runDir}/\${NAME}.toml"
15457
+ cat > "$TOML" <<EOF
15458
+ serverAddr = "\${FACTIII_FRP_SERVER}"
15459
+ serverPort = \${FACTIII_FRP_PORT}
15460
+ transport.protocol = "tcp"
15461
+
15462
+ [metadatas]
15463
+ factiii_token = "\${FACTIII_FRP_TOKEN}"
15464
+
15465
+ [[proxies]]
15466
+ name = "\${PROXY}"
15467
+ type = "http"
15468
+ localPort = \${PORT}
15469
+ subdomain = "\${PROXY}"
15470
+ EOF
15471
+ # nohup, not setsid: setsid does not exist on macOS.
15472
+ nohup frpc -c "$TOML" > "${p.runDir}/\${NAME}.log" 2>&1 < /dev/null &
15473
+ echo $! > "${p.runDir}/\${NAME}.pid"
15474
+ echo "http://\${DOMAIN}"
15475
+ `;
15476
+ }
15477
+ function frpcUnexposeScript(p) {
15478
+ return `#!/bin/sh
15479
+ NAME="$1"
15480
+ if [ -z "$NAME" ]; then
15481
+ echo "usage: factiii-unexpose <name>" >&2
15482
+ exit 1
15483
+ fi
15484
+ PID_FILE="${p.runDir}/\${NAME}.pid"
15485
+ if [ -f "$PID_FILE" ]; then
15486
+ kill "$(cat "$PID_FILE")" 2>/dev/null || true
15487
+ rm -f "$PID_FILE" "${p.runDir}/\${NAME}.toml" "${p.runDir}/\${NAME}.log"
15488
+ echo "stopped \${NAME}"
15489
+ else
15490
+ echo "no tunnel named \${NAME}" >&2
15491
+ exit 1
15492
+ fi
15493
+ `;
15494
+ }
15495
+
15496
+ // ../../shared/all/helpers/card-ai/frpc.ts
15497
+ var FRP_SERVER_ADDR = "dev.factiii.com";
15498
+ var FRP_SERVER_PORT = 7443;
15499
+ var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
15500
+ function frpcPaths(target) {
15501
+ const { root, bin } = target.paths;
15502
+ return {
15503
+ binDir: bin,
15504
+ binary: `${bin}/frpc`,
15505
+ expose: `${bin}/factiii-expose`,
15506
+ unexpose: `${bin}/factiii-unexpose`,
15507
+ envFile: `${root}/.factiii-frp-env`,
15508
+ runDir: `${root}/.frpc`
15509
+ };
15510
+ }
15511
+ function vendoredBinaryName() {
15512
+ const cpu = process.arch === "x64" ? "amd64" : process.arch;
15513
+ return `frpc-${process.platform}-${cpu}`;
15514
+ }
15515
+ async function installFrpcBinary(target) {
15516
+ const p = frpcPaths(target);
15517
+ const binaryName = vendoredBinaryName();
15518
+ const src = import_path6.default.join(__dirname, "..", "vendor", binaryName);
15519
+ if (!(0, import_fs2.existsSync)(src)) {
15520
+ throw new Error(
15521
+ `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
15522
+ );
15523
+ }
15524
+ const staged = await import_promises4.default.stat(p.binary).catch(() => null);
15525
+ const source = await import_promises4.default.stat(src);
15526
+ if (staged?.size === source.size) return;
15527
+ await import_promises4.default.mkdir(p.binDir, { recursive: true });
15528
+ await import_promises4.default.copyFile(src, p.binary);
15529
+ await import_promises4.default.chmod(p.binary, 493);
15530
+ }
15531
+ var EXPOSE_SKILL_NAME = "share-a-port";
15532
+ var FENCE = "```";
15533
+ var EXPOSE_SKILL_MD = [
15534
+ "---",
15535
+ `name: ${EXPOSE_SKILL_NAME}`,
15536
+ "description: Give a port running in this workspace a public HTTPS URL, with factiii-expose. TRIGGER when the user asks to share, expose, preview or publish a local server, wants a link to something running on localhost, or needs an external service to reach a local port.",
15537
+ "---",
15538
+ "",
15539
+ `# ${EXPOSE_SKILL_NAME}`,
15540
+ "",
15541
+ "This workspace can put a local port on the public internet. Both commands are",
15542
+ "already on your PATH; nothing needs installing.",
15543
+ "",
15544
+ "## Share a port",
15545
+ "",
15546
+ FENCE + "bash",
15547
+ "factiii-expose <port> <name> # e.g. factiii-expose 3000 app",
15548
+ FENCE,
15549
+ "",
15550
+ "It prints the public URL and returns straight away - the tunnel runs in the",
15551
+ "background and lives until the session ends or you stop it.",
15552
+ "",
15553
+ "- `<name>` may hold letters, numbers and dashes only.",
15554
+ "- Re-running it with the same name replaces that tunnel rather than opening a",
15555
+ " second one, so it is safe to repeat.",
15556
+ "- Start the server FIRST. A URL for a port nothing is listening on answers",
15557
+ " with an error, which reads to the user like a broken tunnel.",
15558
+ "",
15559
+ "## Stop sharing",
15560
+ "",
15561
+ FENCE + "bash",
15562
+ "factiii-unexpose <name>",
15563
+ FENCE,
15564
+ "",
15565
+ "## Before you share",
15566
+ "",
15567
+ "The URL is public and unauthenticated: anyone holding the link reaches",
15568
+ "whatever that port serves. Do not expose a service that fronts real",
15569
+ "credentials, customer data, or a database. If you are unsure whether the user",
15570
+ "wants something reachable from outside, ask first.",
15571
+ "",
15572
+ "Always tell the user the URL you got back. They can also see every live",
15573
+ "tunnel, and stop any of them, in the card's Task Manager panel.",
15574
+ ""
15575
+ ].join("\n");
15576
+ async function installFrpcHarness(params) {
15577
+ const { target, taskId, runnerToken } = params;
15578
+ const p = frpcPaths(target);
15579
+ await installFrpcBinary(target);
15580
+ const envLines = [
15581
+ `FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
15582
+ `FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
15583
+ `FACTIII_FRP_TOKEN=${runnerToken}`,
15584
+ `FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
15585
+ `FACTIII_TASK_ID=${taskId}`
13772
15586
  ];
13773
15587
  const scriptPaths = { envFile: p.envFile, runDir: p.runDir };
13774
15588
  await target.writeFile(p.expose, frpcExposeScript(scriptPaths));
13775
15589
  await target.writeFile(p.unexpose, frpcUnexposeScript(scriptPaths));
13776
15590
  await target.sh(`chmod +x '${p.expose}' '${p.unexpose}'`);
13777
15591
  await target.writeFile(p.envFile, envLines.join("\n") + "\n");
15592
+ await installBundledSkill({
15593
+ name: EXPOSE_SKILL_NAME,
15594
+ content: EXPOSE_SKILL_MD
15595
+ }).catch(() => void 0);
15596
+ }
15597
+ var ROW_SEP = "|";
15598
+ function parseTunnelRows(raw) {
15599
+ const tunnels = [];
15600
+ for (const line of raw.split("\n")) {
15601
+ const [name, port, url2] = line.trim().split(ROW_SEP);
15602
+ if (!name || !url2) continue;
15603
+ const localPort = Number(port);
15604
+ if (!Number.isInteger(localPort) || localPort <= 0) continue;
15605
+ tunnels.push({ name, port: localPort, url: url2 });
15606
+ }
15607
+ return tunnels;
15608
+ }
15609
+ async function listLiveTunnels(target) {
15610
+ const p = frpcPaths(target);
15611
+ const script = [
15612
+ `set -a; . '${p.envFile}' 2>/dev/null || true; set +a`,
15613
+ `cd '${p.runDir}' 2>/dev/null || exit 0`,
15614
+ "for pidfile in *.pid; do",
15615
+ ' [ -e "$pidfile" ] || continue',
15616
+ ' name=$(basename "$pidfile" .pid)',
15617
+ ' pid=$(cat "$pidfile" 2>/dev/null)',
15618
+ ' [ -n "$pid" ] || continue',
15619
+ // The tunnel is whatever frpc is still running; a stale pid file is not one.
15620
+ ' kill -0 "$pid" 2>/dev/null || continue',
15621
+ ' [ -f "$name.toml" ] || continue',
15622
+ ` port=$(sed -n 's/^localPort = *//p' "$name.toml" | head -1)`,
15623
+ ` sub=$(sed -n 's/^subdomain = *//p' "$name.toml" | tr -d '"' | head -1)`,
15624
+ ' [ -n "$port" ] && [ -n "$sub" ] || continue',
15625
+ ` echo "$name${ROW_SEP}$port${ROW_SEP}http://$sub.$FACTIII_FRP_DOMAIN"`,
15626
+ "done"
15627
+ ].join("\n");
15628
+ const out = await target.run(["sh", "-c", script]).catch(() => "");
15629
+ return parseTunnelRows(out).sort((a, b) => a.port - b.port);
13778
15630
  }
13779
15631
 
13780
15632
  // ../../shared/all/helpers/card-ai/claude-mode.ts
@@ -14064,17 +15916,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
14064
15916
  await this.core.ensureSpaceWorkspace();
14065
15917
  await assertAgentSignedIn(session.provider, this.core.target());
14066
15918
  if (hasGit) {
14067
- const { path: path16 } = await addWorktree(
15919
+ const { path: path17 } = await addWorktree(
14068
15920
  this.core.target(),
14069
15921
  session.taskId,
14070
- baseBranch || config.mainBranch || "main"
15922
+ baseBranch || config.mainBranch || "main",
15923
+ "card"
14071
15924
  );
14072
- session.workspacePath = path16;
15925
+ session.workspacePath = path17;
14073
15926
  await setGitIdentity(
14074
15927
  this.core.target(),
14075
15928
  config.gitName,
14076
15929
  config.gitEmail,
14077
- path16
15930
+ path17
14078
15931
  );
14079
15932
  this.emitter.addLog(session, "system", "Worktree ready.");
14080
15933
  } else {
@@ -14296,17 +16149,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
14296
16149
  this.emitter.addLog(session, "system", "Preparing environment...");
14297
16150
  await this.core.ensureSpaceWorkspace();
14298
16151
  await assertAgentSignedIn(session.provider, this.core.target());
14299
- const { path: path16 } = await addWorktree(
16152
+ const { path: path17 } = await addWorktree(
14300
16153
  this.core.target(),
14301
16154
  session.taskId,
14302
- config.mainBranch || "main"
16155
+ config.mainBranch || "main",
16156
+ "card"
14303
16157
  );
14304
- session.workspacePath = path16;
16158
+ session.workspacePath = path17;
14305
16159
  await setGitIdentity(
14306
16160
  this.core.target(),
14307
16161
  config.gitName,
14308
16162
  config.gitEmail,
14309
- path16
16163
+ path17
14310
16164
  );
14311
16165
  await this.putCard(session);
14312
16166
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -14601,6 +16455,150 @@ async function startTmuxSession(opts) {
14601
16455
  };
14602
16456
  }
14603
16457
 
16458
+ // ../../shared/all/helpers/terminal-ai/processes.ts
16459
+ var PS_FORMAT = "pid=,ppid=,pcpu=,rss=,etime=,args=";
16460
+ var PS_FIELDS = 5;
16461
+ function parsePsTable(raw) {
16462
+ const rows = [];
16463
+ for (const line of raw.split("\n")) {
16464
+ const trimmed = line.trim();
16465
+ const parts2 = trimmed.split(/\s+/);
16466
+ if (parts2.length <= PS_FIELDS) continue;
16467
+ const [pid, ppid, cpu, rss, elapsed] = parts2;
16468
+ const pidNum = Number(pid);
16469
+ const ppidNum = Number(ppid);
16470
+ if (!Number.isInteger(pidNum) || !Number.isInteger(ppidNum)) continue;
16471
+ rows.push({
16472
+ pid: pidNum,
16473
+ ppid: ppidNum,
16474
+ cpu: Number(cpu) || 0,
16475
+ rssKb: Number(rss) || 0,
16476
+ elapsed: elapsed ?? "",
16477
+ args: parts2.slice(PS_FIELDS).join(" ")
16478
+ });
16479
+ }
16480
+ return rows;
16481
+ }
16482
+ function descendantsOf(rows, roots) {
16483
+ const children = /* @__PURE__ */ new Map();
16484
+ for (const row of rows) {
16485
+ const list = children.get(row.ppid);
16486
+ if (list) list.push(row);
16487
+ else children.set(row.ppid, [row]);
16488
+ }
16489
+ const found = [];
16490
+ const seen = new Set(roots);
16491
+ const stack = [...roots];
16492
+ while (stack.length) {
16493
+ const pid = stack.pop();
16494
+ for (const child of children.get(pid) ?? []) {
16495
+ if (seen.has(child.pid)) continue;
16496
+ seen.add(child.pid);
16497
+ found.push(child);
16498
+ stack.push(child.pid);
16499
+ }
16500
+ }
16501
+ return found.sort((a, b) => a.pid - b.pid);
16502
+ }
16503
+ function commandOf(args) {
16504
+ const first = args.trim().split(/\s+/)[0] ?? "";
16505
+ const base = first.split("/").pop() ?? first;
16506
+ return base || "process";
16507
+ }
16508
+ var SHELLS = /* @__PURE__ */ new Set([
16509
+ "bash",
16510
+ "zsh",
16511
+ "sh",
16512
+ "dash",
16513
+ "fish",
16514
+ "ksh",
16515
+ "tcsh",
16516
+ "csh",
16517
+ "login"
16518
+ ]);
16519
+ function isShell(args) {
16520
+ const name = commandOf(args);
16521
+ return SHELLS.has(name.startsWith("-") ? name.slice(1) : name);
16522
+ }
16523
+ function processesForPane(rows, panePid) {
16524
+ const kids = descendantsOf(rows, [panePid]);
16525
+ const pane = rows.find((row) => row.pid === panePid);
16526
+ if (pane && !isShell(pane.args)) return [pane, ...kids];
16527
+ return kids;
16528
+ }
16529
+ function portOf(address) {
16530
+ const colon = address.lastIndexOf(":");
16531
+ if (colon === -1) return null;
16532
+ const port = Number(address.slice(colon + 1));
16533
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
16534
+ }
16535
+ function parseLsofSockets(raw) {
16536
+ const use = /* @__PURE__ */ new Map();
16537
+ let pid = 0;
16538
+ let address = "";
16539
+ const entry = () => {
16540
+ let found = use.get(pid);
16541
+ if (!found) {
16542
+ found = { ports: [], connections: 0 };
16543
+ use.set(pid, found);
16544
+ }
16545
+ return found;
16546
+ };
16547
+ for (const line of raw.split("\n")) {
16548
+ const kind = line[0];
16549
+ if (kind === "p") {
16550
+ pid = Number(line.slice(1)) || 0;
16551
+ address = "";
16552
+ continue;
16553
+ }
16554
+ if (!pid) continue;
16555
+ if (kind === "n") {
16556
+ address = line.slice(1);
16557
+ continue;
16558
+ }
16559
+ if (!line.startsWith("TST=")) continue;
16560
+ const state = line.slice(4);
16561
+ if (state === "LISTEN") {
16562
+ const port = portOf(address);
16563
+ if (port !== null && !entry().ports.includes(port)) {
16564
+ entry().ports.push(port);
16565
+ }
16566
+ } else if (state === "ESTABLISHED") {
16567
+ entry().connections += 1;
16568
+ }
16569
+ address = "";
16570
+ }
16571
+ return use;
16572
+ }
16573
+ function parseSsListen(raw) {
16574
+ const use = /* @__PURE__ */ new Map();
16575
+ for (const line of raw.split("\n")) {
16576
+ const address = line.trim().split(/\s+/)[3];
16577
+ if (!address) continue;
16578
+ const port = portOf(address);
16579
+ if (port === null) continue;
16580
+ for (const match of line.matchAll(/pid=(\d+)/g)) {
16581
+ const pid = Number(match[1]);
16582
+ if (!Number.isInteger(pid)) continue;
16583
+ const found = use.get(pid) ?? { ports: [], connections: 0 };
16584
+ if (!found.ports.includes(port)) found.ports.push(port);
16585
+ use.set(pid, found);
16586
+ }
16587
+ }
16588
+ return use;
16589
+ }
16590
+ async function socketsFor(target, pids) {
16591
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
16592
+ const lsof = await target.run(["lsof", "-a", "-p", pids.join(","), "-nP", "-iTCP", "-F", "pnT"]).catch((err) => String(err.stdout ?? ""));
16593
+ if (lsof.trim()) return parseLsofSockets(lsof);
16594
+ const ss = await target.run(["ss", "-ltnpH"]).catch(() => "");
16595
+ if (!ss.trim()) return /* @__PURE__ */ new Map();
16596
+ const all = parseSsListen(ss);
16597
+ const wanted = new Set(pids);
16598
+ for (const pid of all.keys()) if (!wanted.has(pid)) all.delete(pid);
16599
+ return all;
16600
+ }
16601
+
14604
16602
  // ../../shared/all/helpers/terminal-ai/watcher.ts
14605
16603
  var PRUNED_DIRS = [
14606
16604
  ".git",
@@ -14625,6 +16623,15 @@ var MAX_BATCH = 200;
14625
16623
  var FLUSH_MS = 120;
14626
16624
  var SELF_WRITE_MS = 2e3;
14627
16625
  var dynamicImport = new Function("s", "return import(s)");
16626
+ var importFs = new Function("s", "return import(s)");
16627
+ async function resolveReal(path17) {
16628
+ try {
16629
+ const { realpath } = await importFs("fs/promises");
16630
+ return await realpath(path17);
16631
+ } catch {
16632
+ return path17;
16633
+ }
16634
+ }
14628
16635
  var loadSubscribe = null;
14629
16636
  function getSubscribe() {
14630
16637
  loadSubscribe ??= dynamicImport("@parcel/watcher").then((m) => m.subscribe).catch(() => null);
@@ -14656,6 +16663,7 @@ var WorkspaceWatcher = class {
14656
16663
  * not come back as "changed on disk". */
14657
16664
  this.selfWrites = /* @__PURE__ */ new Map();
14658
16665
  this.closed = false;
16666
+ this.watchRoot = root;
14659
16667
  }
14660
16668
  /** Fire-and-forget: callers treat starting a watch as synchronous, and
14661
16669
  * nothing can be emitted before `subscribe` resolves anyway. */
@@ -14666,9 +16674,11 @@ var WorkspaceWatcher = class {
14666
16674
  async open() {
14667
16675
  const subscribe = await getSubscribe();
14668
16676
  if (!subscribe) return this.stop();
16677
+ this.watchRoot = await resolveReal(this.root);
16678
+ const exclude = await Promise.all(this.exclude.map(resolveReal));
14669
16679
  try {
14670
16680
  const sub = await subscribe(
14671
- this.root,
16681
+ this.watchRoot,
14672
16682
  (err, events) => {
14673
16683
  if (err) return this.stop();
14674
16684
  for (const event of events) this.onEvent(event);
@@ -14678,7 +16688,7 @@ var WorkspaceWatcher = class {
14678
16688
  ignore: [
14679
16689
  ...PRUNED_DIRS.map((d) => `**/${d}/**`),
14680
16690
  ...NOISE_GLOBS,
14681
- ...this.exclude
16691
+ ...exclude
14682
16692
  ]
14683
16693
  }
14684
16694
  );
@@ -14707,8 +16717,8 @@ var WorkspaceWatcher = class {
14707
16717
  /** Typed kinds, so unlike a bare `fs.watch` rename there is nothing to stat
14708
16718
  * to tell a create from a delete. */
14709
16719
  onEvent(event) {
14710
- if (!event.path.startsWith(this.root)) return;
14711
- const rel = event.path.slice(this.root.length).replace(/^\/+/, "");
16720
+ if (!event.path.startsWith(this.watchRoot)) return;
16721
+ const rel = event.path.slice(this.watchRoot.length).replace(/^\/+/, "");
14712
16722
  if (!rel) return;
14713
16723
  if (event.type === "create") this.record("added", rel);
14714
16724
  else if (event.type === "delete") this.record("removed", rel);
@@ -14733,19 +16743,19 @@ var WorkspaceWatcher = class {
14733
16743
  }
14734
16744
  record(kind, relPath) {
14735
16745
  if (this.closed) return;
14736
- const path16 = "/" + relPath;
14737
- const expiry = this.selfWrites.get(path16);
16746
+ const path17 = "/" + relPath;
16747
+ const expiry = this.selfWrites.get(path17);
14738
16748
  if (expiry !== void 0) {
14739
16749
  if (expiry > Date.now()) return;
14740
- this.selfWrites.delete(path16);
16750
+ this.selfWrites.delete(path17);
14741
16751
  }
14742
16752
  const { added, changed, removed } = this.pending;
14743
- added.delete(path16);
14744
- changed.delete(path16);
14745
- removed.delete(path16);
14746
- if (kind === "added") added.add(path16);
14747
- else if (kind === "changed") changed.add(path16);
14748
- else removed.add(path16);
16753
+ added.delete(path17);
16754
+ changed.delete(path17);
16755
+ removed.delete(path17);
16756
+ if (kind === "added") added.add(path17);
16757
+ else if (kind === "changed") changed.add(path17);
16758
+ else removed.add(path17);
14749
16759
  if (added.size + changed.size + removed.size > MAX_BATCH) {
14750
16760
  this.pending = { ...emptyPending(), resync: true };
14751
16761
  }
@@ -14764,8 +16774,8 @@ var WorkspaceWatcher = class {
14764
16774
  this.pending = emptyPending();
14765
16775
  if (!resync && !added.size && !changed.size && !removed.size) return;
14766
16776
  const now = Date.now();
14767
- for (const [path16, expiry] of this.selfWrites) {
14768
- if (expiry <= now) this.selfWrites.delete(path16);
16777
+ for (const [path17, expiry] of this.selfWrites) {
16778
+ if (expiry <= now) this.selfWrites.delete(path17);
14769
16779
  }
14770
16780
  this.emit({
14771
16781
  added: resync ? [] : [...added],
@@ -14850,16 +16860,15 @@ var TerminalModeEngine = class {
14850
16860
  /** Suppress the change event our own write is about to produce, so a save
14851
16861
  * never comes back to the editor as an outside edit. Normalized to the form
14852
16862
  * the watcher emits, since callers spell the path either way. */
14853
- expectWrite(postId, path16) {
14854
- this.watchers.get(postId)?.expectWrite(posixNormalize(path16));
16863
+ expectWrite(postId, path17) {
16864
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path17));
14855
16865
  }
14856
16866
  // ── Lifecycle / ModeEngine ──
14857
16867
  start(base, args) {
14858
16868
  const session = {
14859
16869
  ...base,
14860
16870
  openedFiles: [],
14861
- buffers: {},
14862
- exposed: []
16871
+ buffers: {}
14863
16872
  };
14864
16873
  this.sessions.set(session.postId, session);
14865
16874
  this.emitter.emitSessionUpdate(session);
@@ -14923,8 +16932,32 @@ var TerminalModeEngine = class {
14923
16932
  this.reservedNames.clear();
14924
16933
  this.sessions.clear();
14925
16934
  }
14926
- teardown(session) {
14927
- return teardownSession(session, this.core.target());
16935
+ /** Every tmux session this post owns, by the `terminal-<postId>-<tab>` name
16936
+ * tmuxName() builds. */
16937
+ async terminalSessionNames(target, postId) {
16938
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
16939
+ const stdout = await target.sh(
16940
+ `${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null || true`
16941
+ ).catch(() => "");
16942
+ return stdout.split("\n").map((line) => line.trim()).filter((name) => name.startsWith(prefix));
16943
+ }
16944
+ /** Kill the post's terminals and everything running inside them. Killing the
16945
+ * tmux session alone leaves a process that reparented away from the shell,
16946
+ * so each pane's process tree is killed first. */
16947
+ async killTerminals(postId) {
16948
+ const target = this.core.target();
16949
+ const names = await this.terminalSessionNames(target, postId);
16950
+ for (const name of names) {
16951
+ await target.sh(
16952
+ `for p in $(${tmuxCli(target)} list-panes -t ${name} -F '#{pane_pid}' 2>/dev/null); do pkill -TERM -P "$p" 2>/dev/null || true; done; ${tmuxCli(target)} kill-session -t ${name} 2>/dev/null || true`
16953
+ ).catch(() => {
16954
+ });
16955
+ }
16956
+ }
16957
+ async teardown(session) {
16958
+ await this.killTerminals(session.postId).catch(() => {
16959
+ });
16960
+ await teardownSession(session, this.core.target());
14928
16961
  }
14929
16962
  putCard(s) {
14930
16963
  return putSessionCard(s, this.core.target());
@@ -14961,8 +16994,7 @@ var TerminalModeEngine = class {
14961
16994
  workspacePath: r.worktreePath,
14962
16995
  agentSessionId: r.agentSessionId,
14963
16996
  openedFiles: [],
14964
- buffers: {},
14965
- exposed: []
16997
+ buffers: {}
14966
16998
  };
14967
16999
  this.sessions.set(r.postId, session);
14968
17000
  this.startWatching(session);
@@ -15004,8 +17036,7 @@ var TerminalModeEngine = class {
15004
17036
  workspacePath: args.workspacePath,
15005
17037
  agentSessionId: args.agentSessionId,
15006
17038
  openedFiles: [],
15007
- buffers: {},
15008
- exposed: []
17039
+ buffers: {}
15009
17040
  };
15010
17041
  this.sessions.set(args.postId, session);
15011
17042
  this.stopWatching(args.postId);
@@ -15025,13 +17056,18 @@ var TerminalModeEngine = class {
15025
17056
  this.reservedNames.values()
15026
17057
  );
15027
17058
  this.reservedNames.set(session.postId, workName);
15028
- const { path: path16 } = await addWorktree(this.core.target(), workName, branch);
15029
- session.workspacePath = path16;
17059
+ const { path: path17 } = await addWorktree(
17060
+ this.core.target(),
17061
+ workName,
17062
+ branch,
17063
+ "terminal"
17064
+ );
17065
+ session.workspacePath = path17;
15030
17066
  await setGitIdentity(
15031
17067
  this.core.target(),
15032
17068
  config.gitName,
15033
17069
  config.gitEmail,
15034
- path16
17070
+ path17
15035
17071
  );
15036
17072
  this.emitter.addLog(session, "system", "Worktree ready.");
15037
17073
  } else {
@@ -15093,19 +17129,23 @@ var TerminalModeEngine = class {
15093
17129
  }
15094
17130
  async bareFsList({
15095
17131
  postId,
15096
- path: path16
17132
+ path: path17
15097
17133
  }) {
15098
17134
  const target = this.getBareTarget(postId);
15099
17135
  const root = this.getBareRoot(postId);
15100
- const abs = this.resolveBarePath(path16, root);
17136
+ const abs = this.resolveBarePath(path17, root);
15101
17137
  const relRoot = abs.slice(root.length) || "/";
15102
- const out = await target.sh(`cd ${shEscape(abs)} && for f in .* *; do
17138
+ const out = await target.run([
17139
+ "sh",
17140
+ "-c",
17141
+ `cd ${shEscape(abs)} && for f in .* *; do
15103
17142
  case "$f" in
15104
17143
  "."|".."|".git"|"*") continue ;;
15105
17144
  esac
15106
17145
  [ -e "$f" ] || continue
15107
17146
  if [ -d "$f" ]; then echo "d/$f"; else echo "f/$f"; fi
15108
- done | LC_ALL=C sort`);
17147
+ done | LC_ALL=C sort`
17148
+ ]);
15109
17149
  const entries = [];
15110
17150
  for (const line of out.split("\n")) {
15111
17151
  if (!line) continue;
@@ -15143,32 +17183,32 @@ var TerminalModeEngine = class {
15143
17183
  }
15144
17184
  async bareFsRead({
15145
17185
  postId,
15146
- path: path16
17186
+ path: path17
15147
17187
  }) {
15148
17188
  const target = this.getBareTarget(postId);
15149
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
17189
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15150
17190
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
15151
17191
  '`)).trim();
15152
17192
  return { content: b64, encoding: "base64" };
15153
17193
  }
15154
17194
  async bareFsWrite({
15155
17195
  postId,
15156
- path: path16,
17196
+ path: path17,
15157
17197
  content,
15158
17198
  encoding
15159
17199
  }) {
15160
17200
  const target = this.getBareTarget(postId);
15161
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
17201
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15162
17202
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
15163
17203
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
15164
17204
  throw new Error("Invalid base64 payload.");
15165
17205
  }
15166
- this.expectWrite(String(postId), path16);
17206
+ this.expectWrite(String(postId), path17);
15167
17207
  await target.sh(
15168
17208
  `mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
15169
17209
  );
15170
17210
  const session = this.sessions.get(String(postId));
15171
- if (session) delete session.buffers[path16];
17211
+ if (session) delete session.buffers[path17];
15172
17212
  }
15173
17213
  /** Write a dropped image to the per-space drops dir (`paths.root`, OUTSIDE the
15174
17214
  * card worktree so it never touches the repo) and return its absolute path. */
@@ -15213,39 +17253,39 @@ var TerminalModeEngine = class {
15213
17253
  }
15214
17254
  bareOpenFile({
15215
17255
  postId: rawPostId,
15216
- path: path16
17256
+ path: path17
15217
17257
  }) {
15218
17258
  const session = this.sessions.get(String(rawPostId));
15219
17259
  if (!session || session.mode !== "bare") return;
15220
- if (!session.openedFiles.includes(path16)) {
15221
- session.openedFiles.push(path16);
17260
+ if (!session.openedFiles.includes(path17)) {
17261
+ session.openedFiles.push(path17);
15222
17262
  }
15223
17263
  }
15224
17264
  bareCloseFile({
15225
17265
  postId: rawPostId,
15226
- path: path16
17266
+ path: path17
15227
17267
  }) {
15228
17268
  const session = this.sessions.get(String(rawPostId));
15229
17269
  if (!session || session.mode !== "bare") return;
15230
- session.openedFiles = session.openedFiles.filter((p) => p !== path16);
15231
- delete session.buffers[path16];
17270
+ session.openedFiles = session.openedFiles.filter((p) => p !== path17);
17271
+ delete session.buffers[path17];
15232
17272
  }
15233
17273
  bareBufferSet({
15234
17274
  postId: rawPostId,
15235
- path: path16,
17275
+ path: path17,
15236
17276
  content
15237
17277
  }) {
15238
17278
  const session = this.sessions.get(String(rawPostId));
15239
17279
  if (!session || session.mode !== "bare") return;
15240
- session.buffers[path16] = content;
17280
+ session.buffers[path17] = content;
15241
17281
  }
15242
17282
  async bareFsDelete({
15243
17283
  postId,
15244
- path: path16
17284
+ path: path17
15245
17285
  }) {
15246
17286
  const target = this.getBareTarget(postId);
15247
17287
  const root = this.getBareRoot(postId);
15248
- const abs = this.resolveBarePath(path16, root);
17288
+ const abs = this.resolveBarePath(path17, root);
15249
17289
  if (abs === root) {
15250
17290
  throw new Error("Refusing to delete the repo root.");
15251
17291
  }
@@ -15253,10 +17293,10 @@ var TerminalModeEngine = class {
15253
17293
  }
15254
17294
  async bareFsMkdir({
15255
17295
  postId,
15256
- path: path16
17296
+ path: path17
15257
17297
  }) {
15258
17298
  const target = this.getBareTarget(postId);
15259
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
17299
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15260
17300
  await target.sh(`mkdir -p ${shEscape(abs)}`);
15261
17301
  }
15262
17302
  async bareReview({
@@ -15317,25 +17357,56 @@ var TerminalModeEngine = class {
15317
17357
  commitMessage: session.commitMessage
15318
17358
  };
15319
17359
  }
15320
- async bareGitStatus({
15321
- postId: rawPostId
15322
- }) {
15323
- const postId = String(rawPostId);
15324
- const session = this.sessions.get(postId);
17360
+ /** The worktree a card's git panel acts on, or '' when the card has none (a
17361
+ * OneDrive-only space has no tree to diff). Throws for a session that isn't
17362
+ * live, so a stale modal gets an error rather than acting on whatever
17363
+ * happens to sit at the space root. */
17364
+ gitWorkdir(rawPostId) {
17365
+ const session = this.sessions.get(String(rawPostId));
15325
17366
  if (!session) throw new Error("No active session for this card.");
15326
17367
  if (session.mode !== "bare") {
15327
- throw new Error("bareGitStatus is only valid in Bare mode.");
17368
+ throw new Error("Git actions are only valid in Bare mode.");
15328
17369
  }
15329
17370
  if (session.phase !== "bare-active" && session.phase !== "bare-review") {
15330
17371
  throw new Error("Workspace is not ready.");
15331
17372
  }
15332
- if (!session.workspacePath) {
15333
- return { diff: "", changedFiles: [] };
15334
- }
15335
- return await extractDiff(
15336
- this.core.target(),
15337
- session.workspacePath || void 0
15338
- );
17373
+ return session.workspacePath || "";
17374
+ }
17375
+ async bareGitStatus({
17376
+ postId
17377
+ }) {
17378
+ const workdir = this.gitWorkdir(postId);
17379
+ if (!workdir) return { diff: "", changedFiles: [] };
17380
+ return await readGitPanel(this.core.target(), workdir);
17381
+ }
17382
+ /** Both staging calls answer with the panel's next state: the index is
17383
+ * inside .git, which the working-tree watcher prunes, so nothing else would
17384
+ * tell the panel its rows just moved. */
17385
+ async bareGitStage({
17386
+ postId: rawPostId,
17387
+ paths
17388
+ }) {
17389
+ const workdir = this.gitWorkdir(rawPostId);
17390
+ if (!workdir) return { diff: "", changedFiles: [] };
17391
+ await stagePaths(this.core.target(), workdir, paths);
17392
+ return await readGitPanel(this.core.target(), workdir);
17393
+ }
17394
+ async bareGitUnstage({
17395
+ postId: rawPostId,
17396
+ paths
17397
+ }) {
17398
+ const workdir = this.gitWorkdir(rawPostId);
17399
+ if (!workdir) return { diff: "", changedFiles: [] };
17400
+ await unstagePaths(this.core.target(), workdir, paths);
17401
+ return await readGitPanel(this.core.target(), workdir);
17402
+ }
17403
+ async bareGitCommitDiff({
17404
+ postId,
17405
+ sha
17406
+ }) {
17407
+ const workdir = this.gitWorkdir(postId);
17408
+ if (!workdir) return { diff: "", changedFiles: [] };
17409
+ return await readCommitDiff(this.core.target(), workdir, sha);
15339
17410
  }
15340
17411
  /** `targetOverride` carries the cross-mode lookup, so tabs resolve for cards
15341
17412
  * this engine doesn't own - the crashed claude sessions users most need. */
@@ -15368,6 +17439,87 @@ var TerminalModeEngine = class {
15368
17439
  return [];
15369
17440
  }
15370
17441
  }
17442
+ /** Pane pids for this card's tabs, keyed by tab id. The pane's own shell is
17443
+ * the root of everything the user started in that tab. */
17444
+ async panePids(target, postId) {
17445
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
17446
+ const stdout = await target.sh(
17447
+ `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_pid}' 2>/dev/null || true`
17448
+ ).catch(() => "");
17449
+ const panes = [];
17450
+ for (const raw of stdout.split("\n")) {
17451
+ const line = raw.trim();
17452
+ if (!line.startsWith(prefix)) continue;
17453
+ const rest = line.slice(prefix.length);
17454
+ const sep = rest.indexOf("|");
17455
+ if (sep === -1) continue;
17456
+ const pid = Number(rest.slice(sep + 1));
17457
+ if (!Number.isInteger(pid) || pid <= 0) continue;
17458
+ panes.push({ tabId: rest.slice(0, sep), pid });
17459
+ }
17460
+ return panes;
17461
+ }
17462
+ async bareListProcesses({
17463
+ postId: rawPostId
17464
+ }) {
17465
+ const postId = String(rawPostId);
17466
+ const target = this.getBareTarget(postId);
17467
+ const panes = await this.panePids(target, postId);
17468
+ if (panes.length === 0) return [];
17469
+ const table = parsePsTable(
17470
+ await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17471
+ );
17472
+ const found = [];
17473
+ for (const pane of panes) {
17474
+ for (const row of processesForPane(table, pane.pid)) {
17475
+ found.push({
17476
+ pid: row.pid,
17477
+ tabId: pane.tabId,
17478
+ command: commandOf(row.args),
17479
+ args: row.args,
17480
+ cpu: row.cpu,
17481
+ memoryKb: row.rssKb,
17482
+ elapsed: row.elapsed,
17483
+ ports: [],
17484
+ connections: 0
17485
+ });
17486
+ }
17487
+ }
17488
+ if (found.length === 0) return [];
17489
+ const sockets = await socketsFor(
17490
+ target,
17491
+ found.map((p) => p.pid)
17492
+ );
17493
+ for (const proc of found) {
17494
+ const use = sockets.get(proc.pid);
17495
+ proc.ports = use?.ports ?? [];
17496
+ proc.connections = use?.connections ?? 0;
17497
+ }
17498
+ return found.sort(
17499
+ (a, b) => (Number(a.tabId) || 0) - (Number(b.tabId) || 0) || a.pid - b.pid
17500
+ );
17501
+ }
17502
+ async bareKillProcess({
17503
+ postId: rawPostId,
17504
+ pid
17505
+ }) {
17506
+ const postId = String(rawPostId);
17507
+ const target = this.getBareTarget(postId);
17508
+ if (!Number.isInteger(pid) || pid <= 1) {
17509
+ throw new Error("Not a process this card can stop.");
17510
+ }
17511
+ const panes = await this.panePids(target, postId);
17512
+ const table = parsePsTable(
17513
+ await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17514
+ );
17515
+ const mine = panes.some(
17516
+ (pane) => processesForPane(table, pane.pid).some((row) => row.pid === pid)
17517
+ );
17518
+ if (!mine) {
17519
+ throw new Error("That process is not running in this card.");
17520
+ }
17521
+ await target.run(["kill", "-TERM", String(pid)]);
17522
+ }
15371
17523
  // ── port forwarding (factiii-expose / factiii-unexpose) ──
15372
17524
  async bareExpose({
15373
17525
  postId,
@@ -15375,7 +17527,6 @@ var TerminalModeEngine = class {
15375
17527
  name
15376
17528
  }) {
15377
17529
  const target = this.getBareTarget(postId);
15378
- const session = this.sessions.get(String(postId));
15379
17530
  const safeName = name.trim();
15380
17531
  if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
15381
17532
  throw new Error("Name must be letters, numbers, or dashes.");
@@ -15388,30 +17539,28 @@ var TerminalModeEngine = class {
15388
17539
  if (!/^https?:\/\//.test(url2)) {
15389
17540
  throw new Error(out || "Failed to expose port.");
15390
17541
  }
15391
- const entry = { name: safeName, port, url: url2 };
15392
- session.exposed = session.exposed.filter((e) => e.name !== safeName);
15393
- session.exposed.push(entry);
15394
- return entry;
17542
+ return { name: safeName, port, url: url2 };
15395
17543
  }
15396
17544
  async bareUnexpose({
15397
17545
  postId,
15398
17546
  name
15399
17547
  }) {
15400
17548
  const target = this.getBareTarget(postId);
15401
- const session = this.sessions.get(String(postId));
15402
17549
  const safeName = name.trim();
15403
17550
  if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
15404
17551
  throw new Error("Invalid tunnel name.");
15405
17552
  }
15406
17553
  await target.sh(`factiii-unexpose ${shEscape(safeName)} 2>&1 || true`);
15407
- session.exposed = session.exposed.filter((e) => e.name !== safeName);
15408
17554
  }
15409
- bareListExposed({
17555
+ /** Derived, never remembered: `factiii-expose` is on the agent's PATH, so the
17556
+ * terminal and the agent both open tunnels without telling us, and a runner
17557
+ * restart would forget any list we kept. */
17558
+ async bareListExposed({
15410
17559
  postId
15411
17560
  }) {
15412
17561
  const session = this.sessions.get(String(postId));
15413
17562
  if (!session || session.mode !== "bare") return [];
15414
- return session.exposed.slice();
17563
+ return await listLiveTunnels(this.core.target());
15415
17564
  }
15416
17565
  };
15417
17566
 
@@ -15689,571 +17838,204 @@ var CodexLoginFlow = class {
15689
17838
  reject(new Error("Codex sign-in cancelled."));
15690
17839
  }
15691
17840
  await this.cleanup();
15692
- }
15693
- onData(chunk) {
15694
- this.output += this.strip(chunk.toString("utf-8"));
15695
- if (this.output.length > 2e5) {
15696
- this.output = this.output.slice(-1e5);
15697
- }
15698
- if (this.startResolver) {
15699
- const url2 = this.output.match(/https:\/\/[^\s\x00-\x1f]+/)?.[0];
15700
- const code = this.output.match(CODE_LINE_RE)?.[1] ?? this.output.match(CODE_TOKEN_RE)?.[1];
15701
- if (url2 && code) {
15702
- const resolve2 = this.startResolver;
15703
- this.startResolver = null;
15704
- this.startRejecter = null;
15705
- resolve2({ url: url2.replace(/[.,;)\]]+$/, ""), code });
15706
- }
15707
- return;
15708
- }
15709
- if (this.settled) return;
15710
- if (/success|logged in/i.test(this.output)) {
15711
- this.settled = true;
15712
- void this.verifyAndComplete("");
15713
- }
15714
- }
15715
- // Process ended (or success was printed): the auth.json on the cache volume
15716
- // is the ground truth for whether the sign-in worked.
15717
- async verifyAndComplete(failTail) {
15718
- const ok = await loginCredentialsExist("codex", this.target);
15719
- await this.cleanup();
15720
- this.onDone(
15721
- ok ? { status: "complete" } : {
15722
- status: "error",
15723
- message: failTail || "Codex sign-in ended without signing in."
15724
- }
15725
- );
15726
- }
15727
- async onClose() {
15728
- if (this.settled) return;
15729
- const tail2 = this.output.trim().slice(-300);
15730
- if (this.startRejecter) {
15731
- this.fail(tail2 || "Codex sign-in ended unexpectedly.");
15732
- return;
15733
- }
15734
- this.settled = true;
15735
- await this.verifyAndComplete(tail2);
15736
- }
15737
- fail(message) {
15738
- if (this.settled) return;
15739
- this.settled = true;
15740
- if (this.startRejecter) {
15741
- const reject = this.startRejecter;
15742
- this.startResolver = null;
15743
- this.startRejecter = null;
15744
- reject(new Error(message));
15745
- void this.cleanup();
15746
- return;
15747
- }
15748
- void this.cleanup();
15749
- this.onDone({ status: "error", message });
15750
- }
15751
- async cleanup() {
15752
- await stopLoginExec(this.target, "codex", this.spaceSlug);
15753
- }
15754
- reset() {
15755
- this.output = "";
15756
- this.strip = createAnsiStripper();
15757
- this.settled = false;
15758
- this.startResolver = null;
15759
- this.startRejecter = null;
15760
- }
15761
- };
15762
-
15763
- // ../../shared/all/helpers/board-agent-core/github-auth.ts
15764
- var DEVICE_CODE_URL = "https://github.com/login/device/code";
15765
- var TOKEN_URL = "https://github.com/login/oauth/access_token";
15766
- var API_BASE = "https://api.github.com";
15767
- var GITHUB_SCOPES = "repo read:user user:email";
15768
- var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
15769
- async function startGithubDeviceCode(clientId) {
15770
- const res = await fetch(DEVICE_CODE_URL, {
15771
- method: "POST",
15772
- headers: {
15773
- "Content-Type": "application/x-www-form-urlencoded",
15774
- Accept: "application/json"
15775
- },
15776
- body: new URLSearchParams({ client_id: clientId, scope: GITHUB_SCOPES })
15777
- });
15778
- if (!res.ok) {
15779
- throw new Error(`GitHub device-code start failed: ${await res.text()}`);
15780
- }
15781
- const data = await res.json();
15782
- if (data.error || !data.device_code || !data.user_code) {
15783
- throw new Error(
15784
- data.error_description || data.error || "GitHub returned no device code. Is device flow enabled on the OAuth App?"
15785
- );
15786
- }
15787
- return {
15788
- deviceCode: data.device_code,
15789
- userCode: data.user_code,
15790
- verificationUri: data.verification_uri || "https://github.com/login/device",
15791
- expiresIn: data.expires_in ?? 900,
15792
- interval: data.interval ?? 5
15793
- };
15794
- }
15795
- async function pollGithubDeviceCode(clientId, deviceCode) {
15796
- const res = await fetch(TOKEN_URL, {
15797
- method: "POST",
15798
- headers: {
15799
- "Content-Type": "application/x-www-form-urlencoded",
15800
- Accept: "application/json"
15801
- },
15802
- body: new URLSearchParams({
15803
- client_id: clientId,
15804
- device_code: deviceCode,
15805
- grant_type: DEVICE_CODE_GRANT
15806
- })
15807
- });
15808
- const data = await res.json();
15809
- if (data.access_token) {
15810
- return { status: "complete", accessToken: data.access_token };
15811
- }
15812
- switch (data.error) {
15813
- case "authorization_pending":
15814
- return { status: "pending" };
15815
- case "slow_down":
15816
- return { status: "slow_down" };
15817
- case "expired_token":
15818
- return { status: "expired" };
15819
- case "access_denied":
15820
- return { status: "declined" };
15821
- default:
15822
- return {
15823
- status: "error",
15824
- message: data.error_description || data.error || "GitHub sign-in failed."
15825
- };
15826
- }
15827
- }
15828
- async function githubApi(token, path16) {
15829
- const res = await fetch(`${API_BASE}${path16}`, {
15830
- headers: {
15831
- Authorization: `Bearer ${token}`,
15832
- Accept: "application/vnd.github+json",
15833
- "X-GitHub-Api-Version": "2022-11-28"
15834
- }
15835
- });
15836
- if (!res.ok) {
15837
- throw new Error(
15838
- `GitHub ${path16} failed (${res.status}): ${await res.text()}`
15839
- );
15840
- }
15841
- return await res.json();
15842
- }
15843
- async function fetchGithubIdentity(token) {
15844
- const user = await githubApi(token, "/user");
15845
- let email = user.email ?? "";
15846
- if (!email) {
15847
- try {
15848
- const emails = await githubApi(token, "/user/emails");
15849
- email = emails.find((e) => e.primary && e.verified)?.email ?? emails.find((e) => e.verified)?.email ?? "";
15850
- } catch {
15851
- }
15852
- }
15853
- if (!email) email = `${user.id}+${user.login}@users.noreply.github.com`;
15854
- return { login: user.login, name: user.name || user.login, email };
15855
- }
15856
- async function listGithubRepos(token) {
15857
- const out = [];
15858
- for (let page = 1; page <= 3; page++) {
15859
- const batch = await githubApi(token, `/user/repos?per_page=100&sort=pushed&page=${page}`);
15860
- for (const r of batch) {
15861
- if (r.permissions && !r.permissions.push) continue;
15862
- out.push({
15863
- fullName: r.full_name,
15864
- // Strip any embedded credentials; buildCloneUrl adds the token.
15865
- cloneUrl: r.clone_url.replace(/\/\/[^@/]*@/, "//"),
15866
- defaultBranch: r.default_branch,
15867
- private: r.private
15868
- });
15869
- }
15870
- if (batch.length < 100) break;
15871
- }
15872
- return out;
15873
- }
15874
- async function listGithubBranches(token, fullName) {
15875
- const branches = await githubApi(
15876
- token,
15877
- `/repos/${fullName}/branches?per_page=100`
15878
- );
15879
- return branches.map((b) => b.name);
15880
- }
15881
-
15882
- // ../../shared/all/helpers/board-agent-core/host-deploy.ts
15883
- var import_child_process3 = require("child_process");
15884
- var import_promises3 = __toESM(require("fs/promises"));
15885
- var import_path5 = __toESM(require("path"));
15886
- var import_util5 = require("util");
15887
- var execFileAsync2 = (0, import_util5.promisify)(import_child_process3.execFile);
15888
- var SKILL_DIR = ".agents/skills/deploy";
15889
- var SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
15890
- var VARIABLES_PATH = `${SKILL_DIR}/required-variables.json`;
15891
- var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
15892
- var CLAUDE_SKILLS_DIR = ".claude/skills";
15893
- var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
15894
- async function ensureClaudeSkillLink(workspace) {
15895
- const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
15896
- try {
15897
- await import_promises3.default.lstat(linkPath);
15898
- return;
15899
- } catch {
15900
- }
15901
- await import_promises3.default.mkdir(import_path5.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
15902
- await import_promises3.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
15903
- }
15904
- var GIT_NO_HELPER = ["-c", "credential.helper="];
15905
- var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
15906
- async function hostGit(cwd, ...args) {
15907
- const { stdout } = await execFileAsync2("git", [...GIT_NO_HELPER, ...args], {
15908
- cwd,
15909
- env: GIT_ENV,
15910
- maxBuffer: 16 * 1024 * 1024
15911
- });
15912
- return stdout;
15913
- }
15914
- async function checkDeployStatus(opts) {
15915
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15916
- const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
15917
- const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
15918
- let cloned = false;
15919
- try {
15920
- await hostGit(workspace, "rev-parse", "--git-dir");
15921
- await hostGit(workspace, "remote", "set-url", "origin", url2);
15922
- } catch {
15923
- await execFileAsync2(
15924
- "git",
15925
- [...GIT_NO_HELPER, "clone", "--branch", branch, url2, workspace],
15926
- { env: GIT_ENV }
15927
- );
15928
- cloned = true;
15929
- }
15930
- if (!cloned) await hostGit(workspace, "fetch", "origin", branch);
15931
- const has = async (refPath) => {
15932
- try {
15933
- await hostGit(workspace, "cat-file", "-e", refPath);
15934
- return true;
15935
- } catch {
15936
- return false;
17841
+ }
17842
+ onData(chunk) {
17843
+ this.output += this.strip(chunk.toString("utf-8"));
17844
+ if (this.output.length > 2e5) {
17845
+ this.output = this.output.slice(-1e5);
15937
17846
  }
15938
- };
15939
- const hasAll = async (ref) => await has(`${ref}:${SKILL_PATH}`) && await has(`${ref}:${VARIABLES_PATH}`) && await has(`${ref}:${CHECKER_PATH}`);
15940
- let pendingBranch = "";
15941
- if (!await hasAll(`origin/${branch}`)) {
15942
- try {
15943
- await hostGit(
15944
- workspace,
15945
- "fetch",
15946
- "origin",
15947
- "+refs/heads/deploy-skill*:refs/remotes/origin/deploy-skill*"
15948
- );
15949
- const refs = await hostGit(
15950
- workspace,
15951
- "for-each-ref",
15952
- "--sort=-committerdate",
15953
- "--format=%(refname:short)",
15954
- "refs/remotes/origin/deploy-skill*"
15955
- );
15956
- for (const ref of refs.split("\n").filter(Boolean)) {
15957
- if (await hasAll(ref)) {
15958
- pendingBranch = ref.replace(/^origin\//, "");
15959
- break;
15960
- }
17847
+ if (this.startResolver) {
17848
+ const url2 = this.output.match(/https:\/\/[^\s\x00-\x1f]+/)?.[0];
17849
+ const code = this.output.match(CODE_LINE_RE)?.[1] ?? this.output.match(CODE_TOKEN_RE)?.[1];
17850
+ if (url2 && code) {
17851
+ const resolve2 = this.startResolver;
17852
+ this.startResolver = null;
17853
+ this.startRejecter = null;
17854
+ resolve2({ url: url2.replace(/[.,;)\]]+$/, ""), code });
15961
17855
  }
15962
- } catch {
17856
+ return;
15963
17857
  }
15964
- }
15965
- const hasVariables = await has(`origin/${branch}:${VARIABLES_PATH}`);
15966
- let variables = [];
15967
- if (hasVariables) {
15968
- try {
15969
- const raw = await hostGit(
15970
- workspace,
15971
- "cat-file",
15972
- "-p",
15973
- `origin/${branch}:${VARIABLES_PATH}`
15974
- );
15975
- variables = external_exports.array(requiredVariableSchema).parse(JSON.parse(raw));
15976
- } catch {
17858
+ if (this.settled) return;
17859
+ if (/success|logged in/i.test(this.output)) {
17860
+ this.settled = true;
17861
+ void this.verifyAndComplete("");
15977
17862
  }
15978
17863
  }
15979
- return {
15980
- hasSkill: await has(`origin/${branch}:${SKILL_PATH}`),
15981
- hasVariables,
15982
- hasChecker: await has(`origin/${branch}:${CHECKER_PATH}`),
15983
- branch,
15984
- pendingBranch,
15985
- variables
15986
- };
15987
- }
15988
- var requiredVariableSchema = external_exports.object({
15989
- key: external_exports.string().min(1),
15990
- title: external_exports.string().catch(""),
15991
- description: external_exports.string().catch(""),
15992
- category: external_exports.string().catch("general"),
15993
- sensitive: external_exports.boolean().catch(true)
15994
- });
15995
- function spaceEnv(spaceDir) {
15996
- const paths = hostSpacePaths(spaceDir);
15997
- return {
15998
- WORKSPACE: paths.workspace,
15999
- BACKUPS_DIR: paths.backups,
16000
- BUILDS_DIR: paths.builds,
16001
- // No keychain credential helper and no interactive git prompts anywhere
16002
- // in the deploy surface (agent sessions + authorized commands): auth
16003
- // comes from the secrets store or the tokened URL, never the keychain.
16004
- GIT_TERMINAL_PROMPT: "0",
16005
- GIT_CONFIG_COUNT: "1",
16006
- GIT_CONFIG_KEY_0: "credential.helper",
16007
- GIT_CONFIG_VALUE_0: ""
16008
- };
16009
- }
16010
- async function readContract(workspace) {
16011
- const skill = await import_promises3.default.readFile(import_path5.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
16012
- throw new Error("The creator wrote no SKILL.md.");
16013
- });
16014
- const checker = await import_promises3.default.readFile(import_path5.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
16015
- throw new Error("The creator wrote no check-environment.sh.");
16016
- });
16017
- try {
16018
- const raw = JSON.parse(
16019
- await import_promises3.default.readFile(import_path5.default.join(workspace, VARIABLES_PATH), "utf-8")
17864
+ // Process ended (or success was printed): the auth.json on the cache volume
17865
+ // is the ground truth for whether the sign-in worked.
17866
+ async verifyAndComplete(failTail) {
17867
+ const ok = await loginCredentialsExist("codex", this.target);
17868
+ await this.cleanup();
17869
+ this.onDone(
17870
+ ok ? { status: "complete" } : {
17871
+ status: "error",
17872
+ message: failTail || "Codex sign-in ended without signing in."
17873
+ }
16020
17874
  );
16021
- return {
16022
- skill,
16023
- checker,
16024
- variables: external_exports.array(requiredVariableSchema).parse(raw)
16025
- };
16026
- } catch {
16027
- throw new Error("The creator wrote no valid required-variables.json.");
16028
- }
16029
- }
16030
- async function generateDeploySkill(opts) {
16031
- const { instructions, sendLog } = opts;
16032
- if (!instructions.trim()) {
16033
- throw new Error("Describe your deployment process first.");
16034
17875
  }
16035
- const status = await checkDeployStatus(opts);
16036
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16037
- sendLog({
16038
- type: "system",
16039
- content: `Resetting workspace to origin/${status.branch}...`
16040
- });
16041
- await hostGit(
16042
- workspace,
16043
- "checkout",
16044
- "-B",
16045
- status.branch,
16046
- `origin/${status.branch}`
16047
- );
16048
- await hostGit(workspace, "clean", "-fd");
16049
- sendLog({
16050
- type: "system",
16051
- content: "Analyzing the repo and writing the deploy skill..."
16052
- });
16053
- const { result } = await spawnAgent(
16054
- opts.provider,
16055
- hostTarget(opts.spaceDir),
16056
- {
16057
- cwd: workspace,
16058
- prompt: buildPrompt(prompts_default.deployCreate, {
16059
- INSTRUCTIONS: instructions
16060
- }),
16061
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
16062
- // auto: the per-action classifier blocks escalations/unrecognized-infra
16063
- // actions - a host session with no human reviewing each command should
16064
- // not run bypassPermissions. (Codex maps this to a workspace-write
16065
- // sandbox; see agent.ts.)
16066
- permissionMode: "auto",
16067
- model: "claude-opus-4-6",
16068
- includePartialMessages: true,
16069
- env: spaceEnv(opts.spaceDir),
16070
- callbacks: { onLog: (entry) => sendLog(entry) }
16071
- }
16072
- );
16073
- const { sessionId } = await result;
16074
- const draft = await readContract(workspace);
16075
- return { ...draft, sessionId };
16076
- }
16077
- async function reviseDeploySkill(opts) {
16078
- const { feedback, sendLog } = opts;
16079
- if (!feedback.trim()) throw new Error("Corrections are empty.");
16080
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16081
- sendLog({ type: "system", content: "Applying your corrections..." });
16082
- const { result } = await spawnAgent(
16083
- opts.provider,
16084
- hostTarget(opts.spaceDir),
16085
- {
16086
- cwd: workspace,
16087
- prompt: buildPrompt(prompts_default.deployCreateRevise, {
16088
- FEEDBACK: feedback
16089
- }),
16090
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
16091
- permissionMode: "auto",
16092
- model: "claude-opus-4-6",
16093
- includePartialMessages: true,
16094
- resumeSessionId: opts.resumeSessionId || void 0,
16095
- env: spaceEnv(opts.spaceDir),
16096
- callbacks: { onLog: (entry) => sendLog(entry) }
17876
+ async onClose() {
17877
+ if (this.settled) return;
17878
+ const tail2 = this.output.trim().slice(-300);
17879
+ if (this.startRejecter) {
17880
+ this.fail(tail2 || "Codex sign-in ended unexpectedly.");
17881
+ return;
16097
17882
  }
16098
- );
16099
- const { sessionId } = await result;
16100
- const draft = await readContract(workspace);
16101
- return { ...draft, sessionId: sessionId || opts.resumeSessionId || "" };
16102
- }
16103
- async function loadSkillDraft(opts) {
16104
- const branch = opts.branch.replace(/[^\w./-]/g, "");
16105
- if (!branch.startsWith("deploy-skill")) {
16106
- throw new Error("Not a deploy-skill branch.");
17883
+ this.settled = true;
17884
+ await this.verifyAndComplete(tail2);
16107
17885
  }
16108
- await checkDeployStatus(opts);
16109
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16110
- await hostGit(
16111
- workspace,
16112
- "fetch",
16113
- "origin",
16114
- `+refs/heads/${branch}:refs/remotes/origin/${branch}`
16115
- );
16116
- await hostGit(workspace, "checkout", "-B", branch, `origin/${branch}`);
16117
- await hostGit(workspace, "clean", "-fd");
16118
- return readContract(workspace);
16119
- }
16120
- async function approveDeploySkill(opts) {
16121
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16122
- await readContract(workspace);
16123
- await ensureClaudeSkillLink(workspace);
16124
- if (opts.gitName)
16125
- await hostGit(workspace, "config", "user.name", opts.gitName);
16126
- if (opts.gitEmail)
16127
- await hostGit(workspace, "config", "user.email", opts.gitEmail);
16128
- if (opts.onBranch) {
16129
- const dirty = await hostGit(workspace, "status", "--porcelain");
16130
- if (dirty.trim()) {
16131
- await hostGit(workspace, "add", "-A");
16132
- await hostGit(workspace, "commit", "-m", "Revise deploy skill");
16133
- await hostGit(workspace, "push", "origin", opts.onBranch);
17886
+ fail(message) {
17887
+ if (this.settled) return;
17888
+ this.settled = true;
17889
+ if (this.startRejecter) {
17890
+ const reject = this.startRejecter;
17891
+ this.startResolver = null;
17892
+ this.startRejecter = null;
17893
+ reject(new Error(message));
17894
+ void this.cleanup();
17895
+ return;
16134
17896
  }
16135
- return { branch: opts.onBranch };
17897
+ void this.cleanup();
17898
+ this.onDone({ status: "error", message });
17899
+ }
17900
+ async cleanup() {
17901
+ await stopLoginExec(this.target, "codex", this.spaceSlug);
17902
+ }
17903
+ reset() {
17904
+ this.output = "";
17905
+ this.strip = createAnsiStripper();
17906
+ this.settled = false;
17907
+ this.startResolver = null;
17908
+ this.startRejecter = null;
17909
+ }
17910
+ };
17911
+
17912
+ // ../../shared/all/helpers/board-agent-core/github-auth.ts
17913
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
17914
+ var TOKEN_URL = "https://github.com/login/oauth/access_token";
17915
+ var API_BASE = "https://api.github.com";
17916
+ var GITHUB_SCOPES = "repo read:user user:email";
17917
+ var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
17918
+ async function startGithubDeviceCode(clientId) {
17919
+ const res = await fetch(DEVICE_CODE_URL, {
17920
+ method: "POST",
17921
+ headers: {
17922
+ "Content-Type": "application/x-www-form-urlencoded",
17923
+ Accept: "application/json"
17924
+ },
17925
+ body: new URLSearchParams({ client_id: clientId, scope: GITHUB_SCOPES })
17926
+ });
17927
+ if (!res.ok) {
17928
+ throw new Error(`GitHub device-code start failed: ${await res.text()}`);
16136
17929
  }
16137
- let target = "deploy-skill";
16138
- try {
16139
- await hostGit(
16140
- workspace,
16141
- "ls-remote",
16142
- "--exit-code",
16143
- "--heads",
16144
- "origin",
16145
- target
17930
+ const data = await res.json();
17931
+ if (data.error || !data.device_code || !data.user_code) {
17932
+ throw new Error(
17933
+ data.error_description || data.error || "GitHub returned no device code. Is device flow enabled on the OAuth App?"
16146
17934
  );
16147
- const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:]/g, "").replace("T", "-");
16148
- target = `${target}-${stamp}`;
16149
- } catch {
16150
17935
  }
16151
- await hostGit(workspace, "checkout", "-B", target);
16152
- await hostGit(workspace, "add", "-A");
16153
- await hostGit(workspace, "commit", "-m", "Add deploy skill");
16154
- await hostGit(workspace, "push", "origin", target);
16155
- return { branch: target };
17936
+ return {
17937
+ deviceCode: data.device_code,
17938
+ userCode: data.user_code,
17939
+ verificationUri: data.verification_uri || "https://github.com/login/device",
17940
+ expiresIn: data.expires_in ?? 900,
17941
+ interval: data.interval ?? 5
17942
+ };
16156
17943
  }
16157
- async function runEnvironmentCheck(opts) {
16158
- const status = await checkDeployStatus(opts);
16159
- if (!status.hasChecker) {
17944
+ async function pollGithubDeviceCode(clientId, deviceCode) {
17945
+ const res = await fetch(TOKEN_URL, {
17946
+ method: "POST",
17947
+ headers: {
17948
+ "Content-Type": "application/x-www-form-urlencoded",
17949
+ Accept: "application/json"
17950
+ },
17951
+ body: new URLSearchParams({
17952
+ client_id: clientId,
17953
+ device_code: deviceCode,
17954
+ grant_type: DEVICE_CODE_GRANT
17955
+ })
17956
+ });
17957
+ const data = await res.json();
17958
+ if (data.access_token) {
17959
+ return { status: "complete", accessToken: data.access_token };
17960
+ }
17961
+ switch (data.error) {
17962
+ case "authorization_pending":
17963
+ return { status: "pending" };
17964
+ case "slow_down":
17965
+ return { status: "slow_down" };
17966
+ case "expired_token":
17967
+ return { status: "expired" };
17968
+ case "access_denied":
17969
+ return { status: "declined" };
17970
+ default:
17971
+ return {
17972
+ status: "error",
17973
+ message: data.error_description || data.error || "GitHub sign-in failed."
17974
+ };
17975
+ }
17976
+ }
17977
+ async function githubApi(token, path17) {
17978
+ const res = await fetch(`${API_BASE}${path17}`, {
17979
+ headers: {
17980
+ Authorization: `Bearer ${token}`,
17981
+ Accept: "application/vnd.github+json",
17982
+ "X-GitHub-Api-Version": "2022-11-28"
17983
+ }
17984
+ });
17985
+ if (!res.ok) {
16160
17986
  throw new Error(
16161
- "No environment checker on the default branch. Generate the deploy skill first."
17987
+ `GitHub ${path17} failed (${res.status}): ${await res.text()}`
16162
17988
  );
16163
17989
  }
16164
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16165
- const script = await hostGit(
16166
- workspace,
16167
- "cat-file",
16168
- "-p",
16169
- `origin/${status.branch}:${CHECKER_PATH}`
16170
- );
16171
- const tmp = import_path5.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
16172
- await import_promises3.default.writeFile(tmp, script, { mode: 448 });
16173
- let ok = true;
16174
- let output = "";
16175
- try {
16176
- const scriptArg = tmp.split(import_path5.default.sep).join("/");
16177
- const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
16178
- cwd: workspace,
16179
- env: { ...process.env, ...spaceEnv(opts.spaceDir) },
16180
- timeout: 12e4
16181
- });
16182
- output = res.output;
16183
- if (res.code !== 0) ok = false;
16184
- } finally {
16185
- await import_promises3.default.rm(tmp, { force: true });
16186
- }
16187
- const checks = [];
16188
- for (const line of output.split("\n")) {
16189
- const match = /^(OK|FAIL):\s*(.+)$/.exec(line.trim());
16190
- if (!match) continue;
16191
- const [, verdict, rest] = match;
16192
- const dash = rest.indexOf(" - ");
16193
- checks.push({
16194
- ok: verdict === "OK",
16195
- label: dash > 0 ? rest.slice(0, dash) : rest,
16196
- detail: dash > 0 ? rest.slice(dash + 3) : ""
16197
- });
16198
- if (verdict === "FAIL") ok = false;
17990
+ return await res.json();
17991
+ }
17992
+ async function fetchGithubIdentity(token) {
17993
+ const user = await githubApi(token, "/user");
17994
+ let email = user.email ?? "";
17995
+ if (!email) {
17996
+ try {
17997
+ const emails = await githubApi(token, "/user/emails");
17998
+ email = emails.find((e) => e.primary && e.verified)?.email ?? emails.find((e) => e.verified)?.email ?? "";
17999
+ } catch {
18000
+ }
16199
18001
  }
16200
- return { ok, checks, output: output.trim().slice(-4e3) };
18002
+ if (!email) email = `${user.id}+${user.login}@users.noreply.github.com`;
18003
+ return { login: user.login, name: user.name || user.login, email };
16201
18004
  }
16202
- async function fixEnvironment(opts) {
16203
- const { sendLog } = opts;
16204
- const before = await runEnvironmentCheck(opts);
16205
- if (before.ok) return before;
16206
- const status = await checkDeployStatus(opts);
16207
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16208
- const script = await hostGit(
16209
- workspace,
16210
- "cat-file",
16211
- "-p",
16212
- `origin/${status.branch}:${CHECKER_PATH}`
16213
- );
16214
- const failed = before.checks.filter((c) => !c.ok).map((c) => `- ${c.label}${c.detail ? ` (${c.detail})` : ""}`).join("\n");
16215
- sendLog({
16216
- type: "system",
16217
- content: "Setting this machine up to pass the environment check..."
16218
- });
16219
- const { result } = await spawnAgent(
16220
- opts.provider,
16221
- hostTarget(opts.spaceDir),
16222
- {
16223
- cwd: workspace,
16224
- prompt: buildPrompt(prompts_default.deployEnvFix, {
16225
- CHECKS: failed || before.output.slice(-2e3),
16226
- SCRIPT: script
16227
- }),
16228
- allowedTools: "Bash,Read,Grep,Glob,WebSearch,WebFetch",
16229
- // auto: installs are in-scope of the owner's explicit "fix environment"
16230
- // request; the classifier still blocks anything beyond it.
16231
- permissionMode: "auto",
16232
- model: "claude-opus-4-6",
16233
- includePartialMessages: true,
16234
- env: spaceEnv(opts.spaceDir),
16235
- callbacks: { onLog: (entry) => sendLog(entry) }
18005
+ async function listGithubRepos(token) {
18006
+ const out = [];
18007
+ for (let page = 1; page <= 3; page++) {
18008
+ const batch = await githubApi(token, `/user/repos?per_page=100&sort=pushed&page=${page}`);
18009
+ for (const r of batch) {
18010
+ if (r.permissions && !r.permissions.push) continue;
18011
+ out.push({
18012
+ fullName: r.full_name,
18013
+ // Strip any embedded credentials; buildCloneUrl adds the token.
18014
+ cloneUrl: r.clone_url.replace(/\/\/[^@/]*@/, "//"),
18015
+ defaultBranch: r.default_branch,
18016
+ private: r.private
18017
+ });
16236
18018
  }
16237
- );
16238
- await result;
16239
- sendLog({ type: "system", content: "Re-running the environment check..." });
16240
- return runEnvironmentCheck(opts);
18019
+ if (batch.length < 100) break;
18020
+ }
18021
+ return out;
16241
18022
  }
16242
- async function discardDeploySkill(opts) {
16243
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
16244
- await hostGit(workspace, "checkout", "--", ".").catch(() => null);
16245
- await hostGit(workspace, "clean", "-fd");
16246
- return { success: true };
18023
+ async function listGithubBranches(token, fullName) {
18024
+ const branches = await githubApi(
18025
+ token,
18026
+ `/repos/${fullName}/branches?per_page=100`
18027
+ );
18028
+ return branches.map((b) => b.name);
16247
18029
  }
16248
18030
 
16249
18031
  // ../../shared/all/helpers/board-agent-core/host-run.ts
16250
- var import_path7 = __toESM(require("path"));
18032
+ var import_path8 = __toESM(require("path"));
16251
18033
 
16252
18034
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
16253
18035
  var import_child_process4 = require("child_process");
16254
18036
  var import_crypto2 = __toESM(require("crypto"));
16255
- var import_promises4 = __toESM(require("fs/promises"));
16256
- var import_path6 = __toESM(require("path"));
18037
+ var import_promises5 = __toESM(require("fs/promises"));
18038
+ var import_path7 = __toESM(require("path"));
16257
18039
  var import_util6 = require("util");
16258
18040
  var execFileAsync3 = (0, import_util6.promisify)(import_child_process4.execFile);
16259
18041
  var IDENTITY_FILE = "age-identity.enc";
@@ -16286,7 +18068,7 @@ function deriveKey(password, salt) {
16286
18068
  }
16287
18069
  async function secretsInitialized(configDir) {
16288
18070
  try {
16289
- await import_promises4.default.access(import_path6.default.join(configDir, IDENTITY_FILE));
18071
+ await import_promises5.default.access(import_path7.default.join(configDir, IDENTITY_FILE));
16290
18072
  return true;
16291
18073
  } catch {
16292
18074
  return false;
@@ -16325,10 +18107,10 @@ async function initSecrets(configDir, password) {
16325
18107
  tag: cipher.getAuthTag().toString("base64"),
16326
18108
  data: data.toString("base64")
16327
18109
  };
16328
- await import_promises4.default.writeFile(import_path6.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18110
+ await import_promises5.default.writeFile(import_path7.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
16329
18111
  mode: 384
16330
18112
  });
16331
- await import_promises4.default.writeFile(import_path6.default.join(configDir, RECIPIENT_FILE), `${recipient}
18113
+ await import_promises5.default.writeFile(import_path7.default.join(configDir, RECIPIENT_FILE), `${recipient}
16332
18114
  `, {
16333
18115
  mode: 384
16334
18116
  });
@@ -16351,17 +18133,17 @@ function decryptIdentity(box, password) {
16351
18133
  }
16352
18134
  }
16353
18135
  function envPath(spaceDir) {
16354
- return import_path6.default.join(spaceDir, "secrets", ENV_FILE);
18136
+ return import_path7.default.join(spaceDir, "secrets", ENV_FILE);
16355
18137
  }
16356
18138
  async function identityFor(configDir, password) {
16357
18139
  const box = JSON.parse(
16358
- await import_promises4.default.readFile(import_path6.default.join(configDir, IDENTITY_FILE), "utf-8")
18140
+ await import_promises5.default.readFile(import_path7.default.join(configDir, IDENTITY_FILE), "utf-8")
16359
18141
  );
16360
18142
  return decryptIdentity(box, password);
16361
18143
  }
16362
18144
  async function readEnv(spaceDir, identity) {
16363
18145
  try {
16364
- await import_promises4.default.access(envPath(spaceDir));
18146
+ await import_promises5.default.access(envPath(spaceDir));
16365
18147
  } catch {
16366
18148
  return {};
16367
18149
  }
@@ -16390,14 +18172,14 @@ async function readEnv(spaceDir, identity) {
16390
18172
  return values;
16391
18173
  }
16392
18174
  async function writeEnv(configDir, spaceDir, identity, values) {
16393
- const recipient = (await import_promises4.default.readFile(import_path6.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18175
+ const recipient = (await import_promises5.default.readFile(import_path7.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
16394
18176
  const plain = `${Object.entries(values).map(([k, v]) => `${k}=${v}`).join("\n")}
16395
18177
  `;
16396
18178
  const tmp = envPath(spaceDir).replace(
16397
18179
  /\.enc\.env$/,
16398
18180
  `.${process.pid}.tmp.env`
16399
18181
  );
16400
- await import_promises4.default.writeFile(tmp, plain, { mode: 384 });
18182
+ await import_promises5.default.writeFile(tmp, plain, { mode: 384 });
16401
18183
  try {
16402
18184
  const { stdout } = await execFileAsync3(
16403
18185
  "sops",
@@ -16416,9 +18198,9 @@ async function writeEnv(configDir, spaceDir, identity, values) {
16416
18198
  maxBuffer: 4 * 1024 * 1024
16417
18199
  }
16418
18200
  );
16419
- await import_promises4.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18201
+ await import_promises5.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
16420
18202
  } finally {
16421
- await import_promises4.default.rm(tmp, { force: true });
18203
+ await import_promises5.default.rm(tmp, { force: true });
16422
18204
  }
16423
18205
  }
16424
18206
  async function listSecretKeys(configDir, spaceDir, password) {
@@ -16484,8 +18266,8 @@ function tailMarker(text) {
16484
18266
  }
16485
18267
  async function startDeployRun(opts) {
16486
18268
  const { hooks } = opts;
16487
- const workspace = import_path7.default.join(opts.spaceDir, "workspace");
16488
- const configDir = import_path7.default.dirname(opts.spaceDir);
18269
+ const workspace = import_path8.default.join(opts.spaceDir, "workspace");
18270
+ const configDir = import_path8.default.dirname(opts.spaceDir);
16489
18271
  const status = await checkDeployStatus(opts);
16490
18272
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
16491
18273
  throw new Error(
@@ -17365,6 +19147,48 @@ var BoardAgentEngine = class _BoardAgentEngine {
17365
19147
  githubToken: config.githubToken
17366
19148
  });
17367
19149
  }
19150
+ // ── Agent skills (SKILL.md) ──
19151
+ // Repo opts are optional here, unlike deploy: the global roots are readable
19152
+ // with no repo configured, and listSkills reports repoAvailable: false.
19153
+ skillOpts() {
19154
+ const config = this.core.readConfig();
19155
+ return {
19156
+ spaceDir: this.core.spaceDir(),
19157
+ repoUrl: config.repoUrl,
19158
+ mainBranch: config.mainBranch,
19159
+ githubToken: config.githubToken
19160
+ };
19161
+ }
19162
+ skillsList() {
19163
+ return listSkills(this.skillOpts());
19164
+ }
19165
+ skillRead(payload) {
19166
+ return readSkill({ ...this.skillOpts(), ...payload });
19167
+ }
19168
+ skillWriteGlobal(payload) {
19169
+ return writeGlobalSkill(payload);
19170
+ }
19171
+ skillDeleteGlobal(payload) {
19172
+ return deleteGlobalSkill(payload);
19173
+ }
19174
+ skillMigrate(payload) {
19175
+ const config = this.core.readConfig();
19176
+ return migrateSkills({
19177
+ ...this.skillOpts(),
19178
+ ...payload,
19179
+ gitName: config.gitName,
19180
+ gitEmail: config.gitEmail
19181
+ });
19182
+ }
19183
+ skillProposeRepo(payload) {
19184
+ const config = this.core.readConfig();
19185
+ return proposeRepoSkill({
19186
+ ...this.skillOpts(),
19187
+ ...payload,
19188
+ gitName: config.gitName,
19189
+ gitEmail: config.gitEmail
19190
+ });
19191
+ }
17368
19192
  deployCreatorState() {
17369
19193
  return this.creatorState;
17370
19194
  }
@@ -17747,7 +19571,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
17747
19571
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
17748
19572
  secretsDirs() {
17749
19573
  const spaceDir = this.core.spaceDir();
17750
- return { configDir: (0, import_path8.dirname)(spaceDir), spaceDir };
19574
+ return { configDir: (0, import_path9.dirname)(spaceDir), spaceDir };
17751
19575
  }
17752
19576
  async deploySecretsStatus() {
17753
19577
  return {
@@ -18115,6 +19939,21 @@ var BoardAgentEngine = class _BoardAgentEngine {
18115
19939
  bareGitStatus(p) {
18116
19940
  return this.terminal.bareGitStatus(p);
18117
19941
  }
19942
+ bareGitCommitDiff(p) {
19943
+ return this.terminal.bareGitCommitDiff(p);
19944
+ }
19945
+ bareGitStage(p) {
19946
+ return this.terminal.bareGitStage(p);
19947
+ }
19948
+ bareGitUnstage(p) {
19949
+ return this.terminal.bareGitUnstage(p);
19950
+ }
19951
+ bareListProcesses(p) {
19952
+ return this.terminal.bareListProcesses(p);
19953
+ }
19954
+ bareKillProcess(p) {
19955
+ return this.terminal.bareKillProcess(p);
19956
+ }
18118
19957
  bareListTerminals(p) {
18119
19958
  return this.terminal.bareListTerminals(
18120
19959
  p,
@@ -18537,6 +20376,28 @@ ${c.content.trim() || "(no description)"}`
18537
20376
  return await this.bareWorkNames();
18538
20377
  case "deployStatus":
18539
20378
  return await this.deployStatus();
20379
+ case "skillsList":
20380
+ return await this.skillsList();
20381
+ case "skillRead":
20382
+ return await this.skillRead(
20383
+ payload
20384
+ );
20385
+ case "skillWriteGlobal":
20386
+ return await this.skillWriteGlobal(
20387
+ payload
20388
+ );
20389
+ case "skillDeleteGlobal":
20390
+ return await this.skillDeleteGlobal(
20391
+ payload
20392
+ );
20393
+ case "skillMigrate":
20394
+ return await this.skillMigrate(
20395
+ payload
20396
+ );
20397
+ case "skillProposeRepo":
20398
+ return await this.skillProposeRepo(
20399
+ payload
20400
+ );
18540
20401
  case "deployCreateSkill":
18541
20402
  return await this.deployCreateSkill(
18542
20403
  payload,
@@ -18700,6 +20561,27 @@ ${c.content.trim() || "(no description)"}`
18700
20561
  return await this.bareGitStatus(
18701
20562
  payload
18702
20563
  );
20564
+ case "bareGitCommitDiff":
20565
+ return await this.bareGitCommitDiff(
20566
+ payload
20567
+ );
20568
+ case "bareGitStage":
20569
+ return await this.bareGitStage(
20570
+ payload
20571
+ );
20572
+ case "bareGitUnstage":
20573
+ return await this.bareGitUnstage(
20574
+ payload
20575
+ );
20576
+ case "bareListProcesses":
20577
+ return await this.bareListProcesses(
20578
+ payload
20579
+ );
20580
+ case "bareKillProcess":
20581
+ await this.bareKillProcess(
20582
+ payload
20583
+ );
20584
+ return void 0;
18703
20585
  case "bareListTerminals":
18704
20586
  return await this.bareListTerminals(
18705
20587
  payload
@@ -18733,7 +20615,7 @@ ${c.content.trim() || "(no description)"}`
18733
20615
  );
18734
20616
  return void 0;
18735
20617
  case "bareListExposed":
18736
- return this.bareListExposed(
20618
+ return await this.bareListExposed(
18737
20619
  payload
18738
20620
  );
18739
20621
  case "secureStatus":
@@ -18748,9 +20630,9 @@ ${c.content.trim() || "(no description)"}`
18748
20630
  var import_child_process5 = require("child_process");
18749
20631
  var import_crypto3 = require("crypto");
18750
20632
  var import_fs3 = require("fs");
18751
- var import_promises5 = __toESM(require("fs/promises"));
20633
+ var import_promises6 = __toESM(require("fs/promises"));
18752
20634
  var import_net2 = require("net");
18753
- var import_path9 = require("path");
20635
+ var import_path10 = require("path");
18754
20636
  var import_util7 = require("util");
18755
20637
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
18756
20638
  var IndexService = class {
@@ -18809,7 +20691,7 @@ var IndexService = class {
18809
20691
  const log = await this.openLog();
18810
20692
  this.proc = (0, import_child_process5.spawn)(
18811
20693
  process.execPath,
18812
- [(0, import_path9.join)(this.opts.indexDir, "index.js")],
20694
+ [(0, import_path10.join)(this.opts.indexDir, "index.js")],
18813
20695
  {
18814
20696
  cwd: this.opts.indexDir,
18815
20697
  env: {
@@ -18837,8 +20719,8 @@ var IndexService = class {
18837
20719
  * opened (a missing log must never stop the service from starting). */
18838
20720
  async openLog() {
18839
20721
  try {
18840
- await import_promises5.default.mkdir(this.opts.dataDir, { recursive: true });
18841
- return (0, import_fs3.openSync)((0, import_path9.join)(this.opts.dataDir, "index.log"), "a");
20722
+ await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true });
20723
+ return (0, import_fs3.openSync)((0, import_path10.join)(this.opts.dataDir, "index.log"), "a");
18842
20724
  } catch {
18843
20725
  return null;
18844
20726
  }
@@ -18847,10 +20729,10 @@ var IndexService = class {
18847
20729
  * platform needing node-gyp fails here rather than at query time. */
18848
20730
  async installDeps() {
18849
20731
  const dir = this.opts.indexDir;
18850
- if ((0, import_fs3.existsSync)((0, import_path9.join)(dir, "node_modules"))) return;
18851
- await import_promises5.default.copyFile(
18852
- (0, import_path9.join)(dir, "image-package.json"),
18853
- (0, import_path9.join)(dir, "package.json")
20732
+ if ((0, import_fs3.existsSync)((0, import_path10.join)(dir, "node_modules"))) return;
20733
+ await import_promises6.default.copyFile(
20734
+ (0, import_path10.join)(dir, "image-package.json"),
20735
+ (0, import_path10.join)(dir, "package.json")
18854
20736
  );
18855
20737
  await execFileAsync4(
18856
20738
  "npm",
@@ -18862,15 +20744,15 @@ var IndexService = class {
18862
20744
  * CLIs' persistent MCP config, so a fresh port each start would leave every
18863
20745
  * registration pointing at a dead one. */
18864
20746
  async stablePort() {
18865
- const file = (0, import_path9.join)(this.opts.dataDir, ".port");
18866
- const saved = Number(await import_promises5.default.readFile(file, "utf-8").catch(() => ""));
20747
+ const file = (0, import_path10.join)(this.opts.dataDir, ".port");
20748
+ const saved = Number(await import_promises6.default.readFile(file, "utf-8").catch(() => ""));
18867
20749
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
18868
20750
  return saved;
18869
20751
  }
18870
20752
  const port = await freePort();
18871
- await import_promises5.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
20753
+ await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
18872
20754
  });
18873
- await import_promises5.default.writeFile(file, String(port), "utf-8").catch(() => {
20755
+ await import_promises6.default.writeFile(file, String(port), "utf-8").catch(() => {
18874
20756
  });
18875
20757
  return port;
18876
20758
  }
@@ -19038,24 +20920,24 @@ async function pairWithBrowser(serverUrl) {
19038
20920
 
19039
20921
  // src/config.ts
19040
20922
  var import_fs7 = __toESM(require("fs"));
19041
- var import_os4 = __toESM(require("os"));
19042
- var import_path14 = __toESM(require("path"));
20923
+ var import_os5 = __toESM(require("os"));
20924
+ var import_path15 = __toESM(require("path"));
19043
20925
 
19044
20926
  // src/secureFile.ts
19045
20927
  var import_crypto6 = __toESM(require("crypto"));
19046
20928
  var import_fs6 = __toESM(require("fs"));
19047
- var import_path13 = __toESM(require("path"));
20929
+ var import_path14 = __toESM(require("path"));
19048
20930
 
19049
20931
  // src/keychain.ts
19050
20932
  var import_child_process7 = require("child_process");
19051
20933
  var import_crypto4 = __toESM(require("crypto"));
19052
20934
  var import_fs4 = __toESM(require("fs"));
19053
- var import_os2 = __toESM(require("os"));
19054
- var import_path10 = __toESM(require("path"));
20935
+ var import_os3 = __toESM(require("os"));
20936
+ var import_path11 = __toESM(require("path"));
19055
20937
  var SERVICE = "factiii-runner";
19056
20938
  var ACCOUNT = "config-encryption-key";
19057
- var DPAPI_KEY_FILE = import_path10.default.join(
19058
- import_os2.default.homedir(),
20939
+ var DPAPI_KEY_FILE = import_path11.default.join(
20940
+ import_os3.default.homedir(),
19059
20941
  ".factiii-runner",
19060
20942
  "config-key.dpapi"
19061
20943
  );
@@ -19160,7 +21042,7 @@ function winWrite(key) {
19160
21042
  "-Command",
19161
21043
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
19162
21044
  ]);
19163
- import_fs4.default.mkdirSync(import_path10.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21045
+ import_fs4.default.mkdirSync(import_path11.default.dirname(DPAPI_KEY_FILE), { recursive: true });
19164
21046
  import_fs4.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
19165
21047
  `, { mode: 384 });
19166
21048
  }
@@ -19324,21 +21206,21 @@ function keychainStatus() {
19324
21206
  // src/secureStore.ts
19325
21207
  var import_crypto5 = __toESM(require("crypto"));
19326
21208
  var import_fs5 = __toESM(require("fs"));
19327
- var import_path12 = __toESM(require("path"));
21209
+ var import_path13 = __toESM(require("path"));
19328
21210
 
19329
21211
  // src/paths.ts
19330
- var import_os3 = __toESM(require("os"));
19331
- var import_path11 = __toESM(require("path"));
19332
- var CONFIG_DIR = import_path11.default.join(import_os3.default.homedir(), ".factiii-runner");
21212
+ var import_os4 = __toESM(require("os"));
21213
+ var import_path12 = __toESM(require("path"));
21214
+ var CONFIG_DIR = import_path12.default.join(import_os4.default.homedir(), ".factiii-runner");
19333
21215
  function safeSlug(spaceSlug) {
19334
21216
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
19335
21217
  }
19336
21218
  function spaceDirPath(spaceSlug) {
19337
- return import_path11.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21219
+ return import_path12.default.join(CONFIG_DIR, safeSlug(spaceSlug));
19338
21220
  }
19339
21221
 
19340
21222
  // src/secureStore.ts
19341
- var VAULT_KEY_FILE = import_path12.default.join(CONFIG_DIR, "vault-key.json");
21223
+ var VAULT_KEY_FILE = import_path13.default.join(CONFIG_DIR, "vault-key.json");
19342
21224
  var SCRYPT_N2 = 1 << 15;
19343
21225
  var MIN_PASSWORD_LENGTH = 8;
19344
21226
  var SecureStoreError = class extends Error {
@@ -19660,7 +21542,7 @@ function parseEnvelope(raw) {
19660
21542
  }
19661
21543
  }
19662
21544
  function atomicWrite(filePath, body) {
19663
- import_fs6.default.mkdirSync(import_path13.default.dirname(filePath), { recursive: true });
21545
+ import_fs6.default.mkdirSync(import_path14.default.dirname(filePath), { recursive: true });
19664
21546
  const tmp = `${filePath}.${process.pid}.tmp`;
19665
21547
  import_fs6.default.writeFileSync(tmp, body, { mode: 384 });
19666
21548
  import_fs6.default.renameSync(tmp, filePath);
@@ -19775,8 +21657,8 @@ function writeBootJson(filePath, value2) {
19775
21657
  }
19776
21658
 
19777
21659
  // src/config.ts
19778
- var CONFIG_DIR2 = import_path14.default.join(import_os4.default.homedir(), ".factiii-runner");
19779
- var CONFIG_FILE = import_path14.default.join(CONFIG_DIR2, "config.json");
21660
+ var CONFIG_DIR2 = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
21661
+ var CONFIG_FILE = import_path15.default.join(CONFIG_DIR2, "config.json");
19780
21662
  function readRunnerConfig() {
19781
21663
  return readBootJson(CONFIG_FILE);
19782
21664
  }
@@ -19948,7 +21830,7 @@ function makeChunkReassembler() {
19948
21830
  var import_crypto7 = require("crypto");
19949
21831
  var import_fs9 = __toESM(require("fs"));
19950
21832
  var import_node_datachannel = require("node-datachannel");
19951
- var import_path16 = __toESM(require("path"));
21833
+ var import_path17 = __toESM(require("path"));
19952
21834
 
19953
21835
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
19954
21836
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -21128,12 +23010,12 @@ function parse2(str) {
21128
23010
  uri.queryKey = queryKey(uri, uri["query"]);
21129
23011
  return uri;
21130
23012
  }
21131
- function pathNames(obj, path16) {
21132
- const regx = /\/{2,9}/g, names = path16.replace(regx, "/").split("/");
21133
- if (path16.slice(0, 1) == "/" || path16.length === 0) {
23013
+ function pathNames(obj, path17) {
23014
+ const regx = /\/{2,9}/g, names = path17.replace(regx, "/").split("/");
23015
+ if (path17.slice(0, 1) == "/" || path17.length === 0) {
21134
23016
  names.splice(0, 1);
21135
23017
  }
21136
- if (path16.slice(-1) == "/") {
23018
+ if (path17.slice(-1) == "/") {
21137
23019
  names.splice(names.length - 1, 1);
21138
23020
  }
21139
23021
  return names;
@@ -21752,7 +23634,7 @@ var protocol2 = Socket.protocol;
21752
23634
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
21753
23635
  var import_debug7 = __toESM(require_src(), 1);
21754
23636
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
21755
- function url(uri, path16 = "", loc) {
23637
+ function url(uri, path17 = "", loc) {
21756
23638
  let obj = uri;
21757
23639
  loc = loc || typeof location !== "undefined" && location;
21758
23640
  if (null == uri)
@@ -21786,7 +23668,7 @@ function url(uri, path16 = "", loc) {
21786
23668
  obj.path = obj.path || "/";
21787
23669
  const ipv6 = obj.host.indexOf(":") !== -1;
21788
23670
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
21789
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path16;
23671
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path17;
21790
23672
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
21791
23673
  return obj;
21792
23674
  }
@@ -23420,8 +25302,8 @@ function lookup(uri, opts) {
23420
25302
  const parsed = url(uri, opts.path || "/socket.io");
23421
25303
  const source = parsed.source;
23422
25304
  const id = parsed.id;
23423
- const path16 = parsed.path;
23424
- const sameNamespace = cache[id] && path16 in cache[id]["nsps"];
25305
+ const path17 = parsed.path;
25306
+ const sameNamespace = cache[id] && path17 in cache[id]["nsps"];
23425
25307
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
23426
25308
  let io;
23427
25309
  if (newConnection) {
@@ -23463,8 +25345,8 @@ function cardContextPath(target, postId) {
23463
25345
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
23464
25346
  return `${target.paths.root}/.cards/${safe}.md`;
23465
25347
  }
23466
- function cardContextPrompt(path16) {
23467
- return `Read ${path16} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
25348
+ function cardContextPrompt(path17) {
25349
+ return `Read ${path17} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
23468
25350
  }
23469
25351
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
23470
25352
  var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
@@ -23629,10 +25511,10 @@ var TerminalManager = class {
23629
25511
 
23630
25512
  // src/agent-adapter.ts
23631
25513
  var import_fs8 = __toESM(require("fs"));
23632
- var import_path15 = __toESM(require("path"));
23633
- var RUNNER_CONFIG_PATH = import_path15.default.join(CONFIG_DIR, "runner-config.json");
25514
+ var import_path16 = __toESM(require("path"));
25515
+ var RUNNER_CONFIG_PATH = import_path16.default.join(CONFIG_DIR, "runner-config.json");
23634
25516
  function connectionPath(spaceSlug) {
23635
- return import_path15.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
25517
+ return import_path16.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
23636
25518
  }
23637
25519
  function writeJson(filePath, value2) {
23638
25520
  writeSecureJson(filePath, value2);
@@ -23666,7 +25548,7 @@ function migrateLegacyConfigs() {
23666
25548
  for (const name of legacy) {
23667
25549
  const slug = name.slice("space-".length, -".json".length);
23668
25550
  const parsed = readSecureJson(
23669
- import_path15.default.join(CONFIG_DIR, name)
25551
+ import_path16.default.join(CONFIG_DIR, name)
23670
25552
  );
23671
25553
  if (!parsed) continue;
23672
25554
  if (!file.defaultSlug) {
@@ -23691,7 +25573,7 @@ function encryptStrayPlaintextConfigs() {
23691
25573
  return;
23692
25574
  }
23693
25575
  for (const name of names) {
23694
- const filePath = import_path15.default.join(CONFIG_DIR, name);
25576
+ const filePath = import_path16.default.join(CONFIG_DIR, name);
23695
25577
  try {
23696
25578
  const raw = import_fs8.default.readFileSync(filePath, "utf-8");
23697
25579
  if (!raw.startsWith("{")) continue;
@@ -23820,7 +25702,7 @@ var localConfigProvider = {
23820
25702
  "claude",
23821
25703
  "codex"
23822
25704
  ]) {
23823
- import_fs8.default.mkdirSync(import_path15.default.join(dir, sub), { recursive: true });
25705
+ import_fs8.default.mkdirSync(import_path16.default.join(dir, sub), { recursive: true });
23824
25706
  }
23825
25707
  return dir;
23826
25708
  }
@@ -23842,6 +25724,7 @@ function toRtcIceServers(servers) {
23842
25724
  continue;
23843
25725
  }
23844
25726
  const [, scheme, hostname, port, transport] = turn;
25727
+ if (scheme === "turns" || transport === "tcp") continue;
23845
25728
  out.push({
23846
25729
  hostname,
23847
25730
  port: Number(port ?? (scheme === "turns" ? 5349 : 3478)),
@@ -23993,15 +25876,15 @@ async function startDaemon(config) {
23993
25876
  }
23994
25877
  );
23995
25878
  const indexCandidates = [
23996
- import_path16.default.join(__dirname, "..", "index"),
23997
- import_path16.default.join(__dirname, "..", "..", "index", "image")
25879
+ import_path17.default.join(__dirname, "..", "index"),
25880
+ import_path17.default.join(__dirname, "..", "..", "index", "image")
23998
25881
  ];
23999
25882
  const indexDir = indexCandidates.find((p) => import_fs9.default.existsSync(p)) ?? indexCandidates[0];
24000
25883
  const odIndex = new IndexService({
24001
25884
  controlToken: (0, import_crypto7.createHash)("sha256").update(config.authToken).digest("hex"),
24002
25885
  embedUrl: () => config.serverUrl,
24003
25886
  indexDir,
24004
- dataDir: import_path16.default.join(CONFIG_DIR, "index-data")
25887
+ dataDir: import_path17.default.join(CONFIG_DIR, "index-data")
24005
25888
  });
24006
25889
  void odIndex.ensureRunning().catch((err) => {
24007
25890
  console.error(
@@ -24151,9 +26034,37 @@ async function startDaemon(config) {
24151
26034
  ).join(", ")}`
24152
26035
  );
24153
26036
  const peer = new import_node_datachannel.PeerConnection("runner", { iceServers });
26037
+ const view = {
26038
+ local: /* @__PURE__ */ new Set(),
26039
+ remote: /* @__PURE__ */ new Set(),
26040
+ relays: 0,
26041
+ gathering: "new",
26042
+ ice: "new"
26043
+ };
26044
+ const candidateType = (candidate) => /\btyp (\w+)/.exec(candidate)?.[1] ?? "unknown";
26045
+ let reported = false;
26046
+ const report = (outcome) => {
26047
+ if (reported || !data.linkId) return;
26048
+ reported = true;
26049
+ socket.emit("runnerLinkReport", {
26050
+ linkId: data.linkId,
26051
+ peerSocketId: data.fromSocketId,
26052
+ outcome,
26053
+ detail: [
26054
+ `ice=${view.ice} gathering=${view.gathering} relays=${view.relays}`,
26055
+ `local=${[...view.local].join(",") || "none"}`,
26056
+ `remote=${[...view.remote].join(",") || "none"}`,
26057
+ // Hostname and port only — the credential never leaves the runner.
26058
+ `turn=${iceServers.map(
26059
+ (u) => typeof u === "string" ? u : `${u.relayType}:${u.hostname}:${u.port}`
26060
+ ).join(" ")}`
26061
+ ].join("\n")
26062
+ });
26063
+ };
24154
26064
  const iceCandidateHandler = (msg) => {
24155
26065
  if (msg.fromSocketId === data.fromSocketId) {
24156
26066
  console.log(`[webrtc] remote ICE candidate: ${msg.candidate.candidate.slice(0, 60)}...`);
26067
+ view.remote.add(candidateType(msg.candidate.candidate));
24157
26068
  peer.addRemoteCandidate(
24158
26069
  msg.candidate.candidate,
24159
26070
  msg.candidate.sdpMid || ""
@@ -24169,18 +26080,31 @@ async function startDaemon(config) {
24169
26080
  };
24170
26081
  peer.onStateChange((state) => {
24171
26082
  console.log(`[webrtc] peer state: ${state}`);
26083
+ view.ice = state;
26084
+ if (state === "connected") report("connected");
24172
26085
  if (state === "failed" || state === "closed" || state === "disconnected") {
26086
+ if (state === "failed") report("failed");
24173
26087
  detachIceHandler();
24174
26088
  }
24175
26089
  });
24176
26090
  peer.onGatheringStateChange((state) => {
24177
26091
  console.log(`[webrtc] gathering state: ${state}`);
26092
+ view.gathering = state;
24178
26093
  });
24179
26094
  peer.onLocalCandidate((candidate, mid) => {
24180
26095
  console.log(`[webrtc] local ICE candidate: ${candidate.slice(0, 60)}...`);
26096
+ const type = candidateType(candidate);
26097
+ view.local.add(type);
26098
+ if (type === "relay") view.relays += 1;
24181
26099
  socket.emit("runnerIceCandidate", {
24182
26100
  targetSocketId: data.fromSocketId,
24183
- candidate: { candidate, sdpMid: mid, sdpMLineIndex: null }
26101
+ // 0 rather than null. A browser accepts a null index, but a mobile
26102
+ // client on the New Architecture passes this straight into codegen'd
26103
+ // JSI bindings that type-check it as a number, and null throws there —
26104
+ // rejecting every candidate we send and stranding the link at
26105
+ // checking. sdpMid identifies the m-line on its own, so the index is
26106
+ // redundant here anyway.
26107
+ candidate: { candidate, sdpMid: mid, sdpMLineIndex: 0 }
24184
26108
  });
24185
26109
  });
24186
26110
  peer.onLocalDescription((sdp, _type) => {