@factiii/runner 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1530 -154
  2. package/index/index.js +2 -2
  3. 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);
@@ -13218,7 +13255,8 @@ async function putSessionCard(s, target, prompt2 = "") {
13218
13255
  }
13219
13256
  async function teardownSession(s, target) {
13220
13257
  if (isWorktreeSession(s, target)) {
13221
- await removeWorktree(target, s.workspacePath).catch(() => {
13258
+ const drop = s.mode === "bare" ? removeWorktreeAndBranch : removeWorktree;
13259
+ await drop(target, s.workspacePath).catch(() => {
13222
13260
  });
13223
13261
  }
13224
13262
  await deleteCard(target, s.postId).catch(() => {
@@ -14064,17 +14102,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
14064
14102
  await this.core.ensureSpaceWorkspace();
14065
14103
  await assertAgentSignedIn(session.provider, this.core.target());
14066
14104
  if (hasGit) {
14067
- const { path: path16 } = await addWorktree(
14105
+ const { path: path17 } = await addWorktree(
14068
14106
  this.core.target(),
14069
14107
  session.taskId,
14070
- baseBranch || config.mainBranch || "main"
14108
+ baseBranch || config.mainBranch || "main",
14109
+ "card"
14071
14110
  );
14072
- session.workspacePath = path16;
14111
+ session.workspacePath = path17;
14073
14112
  await setGitIdentity(
14074
14113
  this.core.target(),
14075
14114
  config.gitName,
14076
14115
  config.gitEmail,
14077
- path16
14116
+ path17
14078
14117
  );
14079
14118
  this.emitter.addLog(session, "system", "Worktree ready.");
14080
14119
  } else {
@@ -14296,17 +14335,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
14296
14335
  this.emitter.addLog(session, "system", "Preparing environment...");
14297
14336
  await this.core.ensureSpaceWorkspace();
14298
14337
  await assertAgentSignedIn(session.provider, this.core.target());
14299
- const { path: path16 } = await addWorktree(
14338
+ const { path: path17 } = await addWorktree(
14300
14339
  this.core.target(),
14301
14340
  session.taskId,
14302
- config.mainBranch || "main"
14341
+ config.mainBranch || "main",
14342
+ "card"
14303
14343
  );
14304
- session.workspacePath = path16;
14344
+ session.workspacePath = path17;
14305
14345
  await setGitIdentity(
14306
14346
  this.core.target(),
14307
14347
  config.gitName,
14308
14348
  config.gitEmail,
14309
- path16
14349
+ path17
14310
14350
  );
14311
14351
  await this.putCard(session);
14312
14352
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -14733,19 +14773,19 @@ var WorkspaceWatcher = class {
14733
14773
  }
14734
14774
  record(kind, relPath) {
14735
14775
  if (this.closed) return;
14736
- const path16 = "/" + relPath;
14737
- const expiry = this.selfWrites.get(path16);
14776
+ const path17 = "/" + relPath;
14777
+ const expiry = this.selfWrites.get(path17);
14738
14778
  if (expiry !== void 0) {
14739
14779
  if (expiry > Date.now()) return;
14740
- this.selfWrites.delete(path16);
14780
+ this.selfWrites.delete(path17);
14741
14781
  }
14742
14782
  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);
14783
+ added.delete(path17);
14784
+ changed.delete(path17);
14785
+ removed.delete(path17);
14786
+ if (kind === "added") added.add(path17);
14787
+ else if (kind === "changed") changed.add(path17);
14788
+ else removed.add(path17);
14749
14789
  if (added.size + changed.size + removed.size > MAX_BATCH) {
14750
14790
  this.pending = { ...emptyPending(), resync: true };
14751
14791
  }
@@ -14764,8 +14804,8 @@ var WorkspaceWatcher = class {
14764
14804
  this.pending = emptyPending();
14765
14805
  if (!resync && !added.size && !changed.size && !removed.size) return;
14766
14806
  const now = Date.now();
14767
- for (const [path16, expiry] of this.selfWrites) {
14768
- if (expiry <= now) this.selfWrites.delete(path16);
14807
+ for (const [path17, expiry] of this.selfWrites) {
14808
+ if (expiry <= now) this.selfWrites.delete(path17);
14769
14809
  }
14770
14810
  this.emit({
14771
14811
  added: resync ? [] : [...added],
@@ -14850,8 +14890,8 @@ var TerminalModeEngine = class {
14850
14890
  /** Suppress the change event our own write is about to produce, so a save
14851
14891
  * never comes back to the editor as an outside edit. Normalized to the form
14852
14892
  * the watcher emits, since callers spell the path either way. */
14853
- expectWrite(postId, path16) {
14854
- this.watchers.get(postId)?.expectWrite(posixNormalize(path16));
14893
+ expectWrite(postId, path17) {
14894
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path17));
14855
14895
  }
14856
14896
  // ── Lifecycle / ModeEngine ──
14857
14897
  start(base, args) {
@@ -14923,8 +14963,32 @@ var TerminalModeEngine = class {
14923
14963
  this.reservedNames.clear();
14924
14964
  this.sessions.clear();
14925
14965
  }
14926
- teardown(session) {
14927
- return teardownSession(session, this.core.target());
14966
+ /** Every tmux session this post owns, by the `terminal-<postId>-<tab>` name
14967
+ * tmuxName() builds. */
14968
+ async terminalSessionNames(target, postId) {
14969
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
14970
+ const stdout = await target.sh(
14971
+ `${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null || true`
14972
+ ).catch(() => "");
14973
+ return stdout.split("\n").map((line) => line.trim()).filter((name) => name.startsWith(prefix));
14974
+ }
14975
+ /** Kill the post's terminals and everything running inside them. Killing the
14976
+ * tmux session alone leaves a process that reparented away from the shell,
14977
+ * so each pane's process tree is killed first. */
14978
+ async killTerminals(postId) {
14979
+ const target = this.core.target();
14980
+ const names = await this.terminalSessionNames(target, postId);
14981
+ for (const name of names) {
14982
+ await target.sh(
14983
+ `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`
14984
+ ).catch(() => {
14985
+ });
14986
+ }
14987
+ }
14988
+ async teardown(session) {
14989
+ await this.killTerminals(session.postId).catch(() => {
14990
+ });
14991
+ await teardownSession(session, this.core.target());
14928
14992
  }
14929
14993
  putCard(s) {
14930
14994
  return putSessionCard(s, this.core.target());
@@ -15025,13 +15089,18 @@ var TerminalModeEngine = class {
15025
15089
  this.reservedNames.values()
15026
15090
  );
15027
15091
  this.reservedNames.set(session.postId, workName);
15028
- const { path: path16 } = await addWorktree(this.core.target(), workName, branch);
15029
- session.workspacePath = path16;
15092
+ const { path: path17 } = await addWorktree(
15093
+ this.core.target(),
15094
+ workName,
15095
+ branch,
15096
+ "terminal"
15097
+ );
15098
+ session.workspacePath = path17;
15030
15099
  await setGitIdentity(
15031
15100
  this.core.target(),
15032
15101
  config.gitName,
15033
15102
  config.gitEmail,
15034
- path16
15103
+ path17
15035
15104
  );
15036
15105
  this.emitter.addLog(session, "system", "Worktree ready.");
15037
15106
  } else {
@@ -15093,11 +15162,11 @@ var TerminalModeEngine = class {
15093
15162
  }
15094
15163
  async bareFsList({
15095
15164
  postId,
15096
- path: path16
15165
+ path: path17
15097
15166
  }) {
15098
15167
  const target = this.getBareTarget(postId);
15099
15168
  const root = this.getBareRoot(postId);
15100
- const abs = this.resolveBarePath(path16, root);
15169
+ const abs = this.resolveBarePath(path17, root);
15101
15170
  const relRoot = abs.slice(root.length) || "/";
15102
15171
  const out = await target.sh(`cd ${shEscape(abs)} && for f in .* *; do
15103
15172
  case "$f" in
@@ -15143,32 +15212,32 @@ var TerminalModeEngine = class {
15143
15212
  }
15144
15213
  async bareFsRead({
15145
15214
  postId,
15146
- path: path16
15215
+ path: path17
15147
15216
  }) {
15148
15217
  const target = this.getBareTarget(postId);
15149
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
15218
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15150
15219
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
15151
15220
  '`)).trim();
15152
15221
  return { content: b64, encoding: "base64" };
15153
15222
  }
15154
15223
  async bareFsWrite({
15155
15224
  postId,
15156
- path: path16,
15225
+ path: path17,
15157
15226
  content,
15158
15227
  encoding
15159
15228
  }) {
15160
15229
  const target = this.getBareTarget(postId);
15161
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
15230
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15162
15231
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
15163
15232
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
15164
15233
  throw new Error("Invalid base64 payload.");
15165
15234
  }
15166
- this.expectWrite(String(postId), path16);
15235
+ this.expectWrite(String(postId), path17);
15167
15236
  await target.sh(
15168
15237
  `mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
15169
15238
  );
15170
15239
  const session = this.sessions.get(String(postId));
15171
- if (session) delete session.buffers[path16];
15240
+ if (session) delete session.buffers[path17];
15172
15241
  }
15173
15242
  /** Write a dropped image to the per-space drops dir (`paths.root`, OUTSIDE the
15174
15243
  * card worktree so it never touches the repo) and return its absolute path. */
@@ -15213,39 +15282,39 @@ var TerminalModeEngine = class {
15213
15282
  }
15214
15283
  bareOpenFile({
15215
15284
  postId: rawPostId,
15216
- path: path16
15285
+ path: path17
15217
15286
  }) {
15218
15287
  const session = this.sessions.get(String(rawPostId));
15219
15288
  if (!session || session.mode !== "bare") return;
15220
- if (!session.openedFiles.includes(path16)) {
15221
- session.openedFiles.push(path16);
15289
+ if (!session.openedFiles.includes(path17)) {
15290
+ session.openedFiles.push(path17);
15222
15291
  }
15223
15292
  }
15224
15293
  bareCloseFile({
15225
15294
  postId: rawPostId,
15226
- path: path16
15295
+ path: path17
15227
15296
  }) {
15228
15297
  const session = this.sessions.get(String(rawPostId));
15229
15298
  if (!session || session.mode !== "bare") return;
15230
- session.openedFiles = session.openedFiles.filter((p) => p !== path16);
15231
- delete session.buffers[path16];
15299
+ session.openedFiles = session.openedFiles.filter((p) => p !== path17);
15300
+ delete session.buffers[path17];
15232
15301
  }
15233
15302
  bareBufferSet({
15234
15303
  postId: rawPostId,
15235
- path: path16,
15304
+ path: path17,
15236
15305
  content
15237
15306
  }) {
15238
15307
  const session = this.sessions.get(String(rawPostId));
15239
15308
  if (!session || session.mode !== "bare") return;
15240
- session.buffers[path16] = content;
15309
+ session.buffers[path17] = content;
15241
15310
  }
15242
15311
  async bareFsDelete({
15243
15312
  postId,
15244
- path: path16
15313
+ path: path17
15245
15314
  }) {
15246
15315
  const target = this.getBareTarget(postId);
15247
15316
  const root = this.getBareRoot(postId);
15248
- const abs = this.resolveBarePath(path16, root);
15317
+ const abs = this.resolveBarePath(path17, root);
15249
15318
  if (abs === root) {
15250
15319
  throw new Error("Refusing to delete the repo root.");
15251
15320
  }
@@ -15253,10 +15322,10 @@ var TerminalModeEngine = class {
15253
15322
  }
15254
15323
  async bareFsMkdir({
15255
15324
  postId,
15256
- path: path16
15325
+ path: path17
15257
15326
  }) {
15258
15327
  const target = this.getBareTarget(postId);
15259
- const abs = this.resolveBarePath(path16, this.getBareRoot(postId));
15328
+ const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
15260
15329
  await target.sh(`mkdir -p ${shEscape(abs)}`);
15261
15330
  }
15262
15331
  async bareReview({
@@ -15825,8 +15894,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
15825
15894
  };
15826
15895
  }
15827
15896
  }
15828
- async function githubApi(token, path16) {
15829
- const res = await fetch(`${API_BASE}${path16}`, {
15897
+ async function githubApi(token, path17) {
15898
+ const res = await fetch(`${API_BASE}${path17}`, {
15830
15899
  headers: {
15831
15900
  Authorization: `Bearer ${token}`,
15832
15901
  Accept: "application/vnd.github+json",
@@ -15835,7 +15904,7 @@ async function githubApi(token, path16) {
15835
15904
  });
15836
15905
  if (!res.ok) {
15837
15906
  throw new Error(
15838
- `GitHub ${path16} failed (${res.status}): ${await res.text()}`
15907
+ `GitHub ${path17} failed (${res.status}): ${await res.text()}`
15839
15908
  );
15840
15909
  }
15841
15910
  return await res.json();
@@ -15911,7 +15980,39 @@ async function hostGit(cwd, ...args) {
15911
15980
  });
15912
15981
  return stdout;
15913
15982
  }
15914
- async function checkDeployStatus(opts) {
15983
+ async function gitCatFileBatch(cwd, shas) {
15984
+ const out = /* @__PURE__ */ new Map();
15985
+ const wanted = [...new Set(shas.filter(Boolean))];
15986
+ if (!wanted.length) return out;
15987
+ const child = (0, import_child_process3.spawn)("git", [...GIT_NO_HELPER, "cat-file", "--batch"], {
15988
+ cwd,
15989
+ env: GIT_ENV
15990
+ });
15991
+ const chunks = [];
15992
+ child.stdout.on("data", (c) => chunks.push(c));
15993
+ const done = new Promise((resolve2) => {
15994
+ child.on("close", () => resolve2());
15995
+ child.on("error", () => resolve2());
15996
+ });
15997
+ child.stdin.end(`${wanted.join("\n")}
15998
+ `);
15999
+ await done;
16000
+ let buf = Buffer.concat(chunks);
16001
+ while (buf.length) {
16002
+ const nl = buf.indexOf(10);
16003
+ if (nl === -1) break;
16004
+ const header = buf.subarray(0, nl).toString("utf-8");
16005
+ buf = buf.subarray(nl + 1);
16006
+ const [sha, type, sizeText] = header.split(" ");
16007
+ if (type === void 0 || sizeText === void 0) continue;
16008
+ const size = Number(sizeText);
16009
+ if (!Number.isFinite(size)) break;
16010
+ out.set(sha, buf.subarray(0, size).toString("utf-8"));
16011
+ buf = buf.subarray(size + 1);
16012
+ }
16013
+ return out;
16014
+ }
16015
+ async function ensureWorkspaceClone(opts) {
15915
16016
  const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15916
16017
  const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
15917
16018
  const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
@@ -15920,6 +16021,15 @@ async function checkDeployStatus(opts) {
15920
16021
  await hostGit(workspace, "rev-parse", "--git-dir");
15921
16022
  await hostGit(workspace, "remote", "set-url", "origin", url2);
15922
16023
  } catch {
16024
+ const leftovers = await import_promises3.default.readdir(workspace).catch(() => []);
16025
+ if (leftovers.includes(".git")) {
16026
+ throw new Error(
16027
+ `${workspace} holds a git repository that cannot be read. Remove it and retry.`
16028
+ );
16029
+ }
16030
+ for (const name of leftovers) {
16031
+ await import_promises3.default.rm(import_path5.default.join(workspace, name), { recursive: true, force: true });
16032
+ }
15923
16033
  await execFileAsync2(
15924
16034
  "git",
15925
16035
  [...GIT_NO_HELPER, "clone", "--branch", branch, url2, workspace],
@@ -15928,6 +16038,10 @@ async function checkDeployStatus(opts) {
15928
16038
  cloned = true;
15929
16039
  }
15930
16040
  if (!cloned) await hostGit(workspace, "fetch", "origin", branch);
16041
+ return { workspace, branch };
16042
+ }
16043
+ async function checkDeployStatus(opts) {
16044
+ const { workspace, branch } = await ensureWorkspaceClone(opts);
15931
16045
  const has = async (refPath) => {
15932
16046
  try {
15933
16047
  await hostGit(workspace, "cat-file", "-e", refPath);
@@ -16621,6 +16735,1151 @@ ${clean}`;
16621
16735
  }
16622
16736
  }
16623
16737
 
16738
+ // ../../shared/all/helpers/board-agent-core/host-skills.ts
16739
+ var import_promises5 = __toESM(require("fs/promises"));
16740
+ var import_os2 = __toESM(require("os"));
16741
+ var import_path8 = __toESM(require("path"));
16742
+
16743
+ // ../../shared/all/domains/posts.ts
16744
+ var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
16745
+ postDataSort2["DAILY"] = "DAILY";
16746
+ postDataSort2["WEEKLY"] = "WEEKLY";
16747
+ postDataSort2["MONTHLY"] = "MONTHLY";
16748
+ return postDataSort2;
16749
+ })(postDataSort || {});
16750
+
16751
+ // ../../shared/all/domains/user.ts
16752
+ var USER_TAG_VALUES = [
16753
+ "HUMAN",
16754
+ "BOT",
16755
+ "GOVERNMENT",
16756
+ "ACADEMIA",
16757
+ "BUSINESS",
16758
+ "NEW",
16759
+ "WAITLIST",
16760
+ "VERIFIED"
16761
+ ];
16762
+ var detailsCookieSchema = external_exports.object({
16763
+ userId: external_exports.number(),
16764
+ updatedAt: external_exports.string(),
16765
+ username: external_exports.string(),
16766
+ name: external_exports.string().nullable(),
16767
+ email: external_exports.string().nullable(),
16768
+ tag: external_exports.enum(USER_TAG_VALUES),
16769
+ avatarKey: external_exports.string().nullable(),
16770
+ robohash: external_exports.string().nullable(),
16771
+ hasPassword: external_exports.boolean(),
16772
+ // Defaulted so a client cookie minted before these fields existed still parses
16773
+ // (the server re-issues it with real values on the next refresh) instead of
16774
+ // failing the whole payload and flashing the user as logged out.
16775
+ hasPasskey: external_exports.boolean().default(false),
16776
+ twoFaEnabled: external_exports.boolean(),
16777
+ oauthProviders: external_exports.array(external_exports.enum(["GOOGLE", "APPLE"])).default([]),
16778
+ // Defaulted, not just nullable: a required-but-missing key is what breaks old
16779
+ // clients, so this stays parseable once the server stops sending it.
16780
+ oauthProvider: external_exports.enum(["GOOGLE", "APPLE"]).nullable().default(null),
16781
+ emailVerificationStatus: external_exports.enum(["UNVERIFIED", "PENDING", "VERIFIED"]),
16782
+ isPrivate: external_exports.boolean(),
16783
+ isIncognito: external_exports.boolean(),
16784
+ onboardingPath: external_exports.string().nullable(),
16785
+ bio: external_exports.string().nullable(),
16786
+ bannerKey: external_exports.string().nullable(),
16787
+ tronsBalance: external_exports.number(),
16788
+ verificationRequestedAt: external_exports.string().nullable(),
16789
+ deletionScheduledAt: external_exports.string().nullable()
16790
+ });
16791
+ var UserType = /* @__PURE__ */ ((UserType2) => {
16792
+ UserType2["PRIVATE"] = "PRIVATE";
16793
+ UserType2["INCOGNITO"] = "INCOGNITO";
16794
+ UserType2["PREMIUM"] = "PREMIUM";
16795
+ UserType2["PAID"] = "PAID";
16796
+ UserType2["INVITE"] = "INVITE";
16797
+ return UserType2;
16798
+ })(UserType || {});
16799
+
16800
+ // ../../shared/all/uploadTypes.ts
16801
+ var _EXT_TO_CONTENT_TYPE = {
16802
+ jpg: "image/jpeg",
16803
+ jpeg: "image/jpeg",
16804
+ png: "image/png",
16805
+ gif: "image/gif",
16806
+ webp: "image/webp",
16807
+ svg: "image/svg+xml",
16808
+ heic: "image/heic",
16809
+ heif: "image/heif",
16810
+ mp4: "video/mp4",
16811
+ mov: "video/quicktime",
16812
+ mpeg: "video/mpeg",
16813
+ pdf: "application/pdf",
16814
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
16815
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
16816
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
16817
+ };
16818
+ var EXT_TO_CONTENT_TYPE = _EXT_TO_CONTENT_TYPE;
16819
+ var ALLOWED_CONTENT_TYPES = [
16820
+ ...new Set(Object.values(EXT_TO_CONTENT_TYPE))
16821
+ ];
16822
+ var ALLOWED_CONTENT_TYPES_TUPLE = ALLOWED_CONTENT_TYPES;
16823
+ var CONTENT_TYPE_TO_EXT = Object.fromEntries(
16824
+ Object.entries(EXT_TO_CONTENT_TYPE).map(([ext, mime]) => [mime, ext])
16825
+ );
16826
+ var DROPZONE_ACCEPT = Object.entries(
16827
+ EXT_TO_CONTENT_TYPE
16828
+ ).reduce((acc, [ext, mime]) => {
16829
+ (acc[mime] ??= []).push(`.${ext}`);
16830
+ return acc;
16831
+ }, {});
16832
+ var FILE_INPUT_ACCEPT = ALLOWED_CONTENT_TYPES.join(",");
16833
+
16834
+ // ../../shared/all/validators.ts
16835
+ var usernameValidationRegex = /^[a-zA-Z0-9_]+$/;
16836
+ var signupSchema = external_exports.object({
16837
+ username: external_exports.string().min(1).regex(usernameValidationRegex, {
16838
+ message: "Username can only contain letters, numbers, and underscores"
16839
+ }),
16840
+ email: external_exports.string().email(),
16841
+ password: external_exports.string().min(8, { message: "Password must contain at least 8 characters" }),
16842
+ types: external_exports.array(external_exports.nativeEnum(UserType)),
16843
+ referrerUserId: external_exports.number().optional(),
16844
+ instanceId: external_exports.number()
16845
+ });
16846
+ var usernameSchema = external_exports.string().min(1).regex(usernameValidationRegex, {
16847
+ message: "Username can only contain letters, numbers, and underscores"
16848
+ });
16849
+ var baseRegisterSchema = external_exports.object({
16850
+ username: usernameSchema,
16851
+ types: external_exports.array(external_exports.nativeEnum(UserType)).default([]),
16852
+ referrerUserId: external_exports.number().optional(),
16853
+ instanceId: external_exports.number(),
16854
+ platform: external_exports.enum(["web", "mobile"]).optional()
16855
+ });
16856
+ var passwordRegisterSchema = baseRegisterSchema.extend({
16857
+ password: external_exports.string().min(8, { message: "Password must contain at least 8 characters" }),
16858
+ recaptchaToken: external_exports.string().optional()
16859
+ });
16860
+ var createSourceSchema = external_exports.object({
16861
+ name: external_exports.string().min(1),
16862
+ description: external_exports.string().optional(),
16863
+ requirements: external_exports.array(
16864
+ external_exports.enum([
16865
+ "GOVERNMENT_SOURCE" /* GOVERNMENT_SOURCE */,
16866
+ "ENTERPRISE_SOURCE" /* ENTERPRISE_SOURCE */,
16867
+ "ANONYMOUS_SOURCE" /* ANONYMOUS_SOURCE */,
16868
+ "HUMAN_SOURCE" /* HUMAN_SOURCE */
16869
+ ])
16870
+ )
16871
+ });
16872
+ var contactFormSchema = external_exports.object({
16873
+ name: external_exports.string().min(1).max(100),
16874
+ email: external_exports.string().max(254).optional(),
16875
+ phone: external_exports.string().max(20).optional(),
16876
+ message: external_exports.string().min(1).max(2e3),
16877
+ age: external_exports.string().optional()
16878
+ }).refine((data) => !!data.email || !!data.phone, {
16879
+ message: "Either email or phone is required"
16880
+ });
16881
+ var donationSchema = external_exports.object({
16882
+ amount: external_exports.number().int().min(100).max(1e6),
16883
+ // Amount in cents (integer)
16884
+ email: external_exports.string().email()
16885
+ });
16886
+ var getUploadUrlsSchema = external_exports.object({
16887
+ contentType: external_exports.enum(ALLOWED_CONTENT_TYPES_TUPLE),
16888
+ name: external_exports.string().optional(),
16889
+ width: external_exports.number().int().positive().max(25e3).optional(),
16890
+ height: external_exports.number().int().positive().max(25e3).optional(),
16891
+ isPrivate: external_exports.boolean().optional().default(false),
16892
+ // Chat attachments pass the conversation instead of an isPrivate flag: the
16893
+ // server resolves privacy from the channel's space so the client can't
16894
+ // decide to make a PRIVATE space's files public. See uploads.getUploadUrls.
16895
+ conversationId: external_exports.string().uuid().optional(),
16896
+ isFactiii: external_exports.boolean().default(false),
16897
+ factiiiId: external_exports.number().int().positive().optional(),
16898
+ rawSha256: external_exports.string().regex(/^[a-f0-9]{64}$/i, "rawSha256 must be 64 hex chars")
16899
+ }).refine(
16900
+ (d) => d.width === void 0 && d.height === void 0 || d.width !== void 0 && d.height !== void 0,
16901
+ { message: "width and height must be provided together" }
16902
+ ).refine((d) => !d.isFactiii || d.factiiiId !== void 0, {
16903
+ message: "factiiiId is required when isFactiii is true"
16904
+ });
16905
+ var finalizeUploadSchema = external_exports.object({
16906
+ uploadId: external_exports.string(),
16907
+ isFactiii: external_exports.boolean(),
16908
+ raw: external_exports.object({
16909
+ // Bare-uuid keys; variant kind is the DB-level discriminator, no S3 prefix.
16910
+ key: external_exports.string().min(1),
16911
+ size: external_exports.number().int().positive(),
16912
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/i),
16913
+ width: external_exports.number().int().positive().optional(),
16914
+ height: external_exports.number().int().positive().optional()
16915
+ }),
16916
+ view: external_exports.object({
16917
+ key: external_exports.string().min(1),
16918
+ size: external_exports.number().int().positive(),
16919
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/i),
16920
+ width: external_exports.number().int().positive(),
16921
+ height: external_exports.number().int().positive()
16922
+ }).nullable()
16923
+ // null for non-image uploads
16924
+ });
16925
+ var variantKindSchema = external_exports.enum([
16926
+ "RAW",
16927
+ "VIEW",
16928
+ "PREMIUM_HD",
16929
+ "PREMIUM_RAW",
16930
+ "THUMBNAIL"
16931
+ ]);
16932
+ var adminUploadsGetVariantUrlSchema = external_exports.object({
16933
+ uploadId: external_exports.string(),
16934
+ kind: variantKindSchema,
16935
+ expiresIn: external_exports.number().int().positive().max(3600).default(300)
16936
+ });
16937
+ var getUploadSchema = external_exports.object({
16938
+ id: external_exports.string()
16939
+ });
16940
+ var priceSchema = external_exports.number().int().min(0, "Price must be a positive integer in cents");
16941
+ var checkPurchaseSchema = external_exports.object({
16942
+ productId: external_exports.string(),
16943
+ type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
16944
+ receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
16945
+ currency: external_exports.string().length(3).default("USD")
16946
+ });
16947
+ var creditPurchaseSchema = external_exports.object({
16948
+ orderId: external_exports.string(),
16949
+ transactionDate: external_exports.number(),
16950
+ receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
16951
+ type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
16952
+ secret: external_exports.string(),
16953
+ // Required for APPLE/GOOGLE IAP — used for server-side receipt verification
16954
+ purchaseToken: external_exports.string().optional()
16955
+ });
16956
+ var dsarFormSchema = external_exports.object({
16957
+ name: external_exports.string().trim().min(1, "Please enter your name.").max(100, "Please keep your name under 100 characters."),
16958
+ email: external_exports.string().trim().min(1, "Please enter your email address.").email("Please enter a valid email address."),
16959
+ selectedAs: external_exports.enum(["person", "agent"]),
16960
+ requestUnder: external_exports.enum(["GDPR", "CCPA", "OTHER"]),
16961
+ selectedTo: external_exports.enum(["knowledge", "delete", "optOut", "optIn", "other"]),
16962
+ details: external_exports.string().trim().min(1, "Please provide details about your request.").max(5e3, "Please keep details under 5000 characters."),
16963
+ confirmAccuracy: external_exports.boolean().refine((value2) => value2, "Please confirm your information is accurate."),
16964
+ confirmConsequences: external_exports.boolean().refine(
16965
+ (value2) => value2,
16966
+ "Please acknowledge the deletion/restriction consequences."
16967
+ ),
16968
+ confirmVerification: external_exports.boolean().refine((value2) => value2, "Please acknowledge the email verification step.")
16969
+ });
16970
+ var mapLinkSchema = external_exports.object({
16971
+ startPosition: external_exports.object({
16972
+ postId: external_exports.string().optional(),
16973
+ latitude: external_exports.number().min(-90).max(90, { message: "Latitude must be between -90 and 90" }),
16974
+ longitude: external_exports.number().min(-180).max(180, { message: "Longitude must be between -180 and 180" })
16975
+ }),
16976
+ pins: external_exports.array(
16977
+ external_exports.object({
16978
+ postId: external_exports.string().uuid({ message: "Pin ID must be a valid UUID" }),
16979
+ latitude: external_exports.number().min(-90).max(90, { message: "Pin latitude must be between -90 and 90" }),
16980
+ longitude: external_exports.number().min(-180).max(180, { message: "Pin longitude must be between -180 and 180" })
16981
+ })
16982
+ ).optional().default([])
16983
+ });
16984
+ var mapLinkPostIds = mapLinkSchema.transform((data) => {
16985
+ return {
16986
+ startPosition: {
16987
+ postId: data.startPosition.postId,
16988
+ latitude: data.startPosition.latitude,
16989
+ longitude: data.startPosition.longitude
16990
+ },
16991
+ pins: data.pins.map((pin) => ({
16992
+ postId: pin.postId,
16993
+ latitude: pin.latitude,
16994
+ longitude: pin.longitude
16995
+ }))
16996
+ };
16997
+ });
16998
+ var mapLinkHref = mapLinkSchema.transform((data) => {
16999
+ const latParam = `latitude=${encodeURIComponent(data.startPosition.latitude)}`;
17000
+ const lonParam = `longitude=${encodeURIComponent(
17001
+ data.startPosition.longitude
17002
+ )}`;
17003
+ const pinsParam = data.pins.length > 0 ? `pins=${encodeURIComponent(JSON.stringify(data.pins))}` : "";
17004
+ return {
17005
+ startPosition: {
17006
+ postId: data.startPosition.postId,
17007
+ latitude: data.startPosition.latitude,
17008
+ longitude: data.startPosition.longitude
17009
+ },
17010
+ href: `${data.startPosition.postId ? `postId=${data.startPosition.postId}&` : ""}${latParam}&${lonParam}${pinsParam ? `&${pinsParam}` : ""}`
17011
+ };
17012
+ });
17013
+ var wikipediaUrlSchema = external_exports.string().refine(
17014
+ (url2) => {
17015
+ try {
17016
+ const parsedUrl = new URL(url2);
17017
+ return parsedUrl.hostname.endsWith(".wikipedia.org") && parsedUrl.protocol === "https:" && parsedUrl.pathname.startsWith("/wiki/") && parsedUrl.pathname.length > 6;
17018
+ } catch {
17019
+ return false;
17020
+ }
17021
+ },
17022
+ {
17023
+ message: "Invalid Wikipedia URL. Must be a valid HTTPS URL from wikipedia.org with /wiki/ path and article title"
17024
+ }
17025
+ );
17026
+ var localPreferenceSchema = external_exports.object({
17027
+ postFilterType: external_exports.enum(["trending", "new", "popular"]),
17028
+ postSortBy: external_exports.nativeEnum(postDataSort),
17029
+ filterBot: external_exports.boolean(),
17030
+ filterNew: external_exports.boolean(),
17031
+ showBoards: external_exports.boolean(),
17032
+ displayMode: external_exports.enum(["data", "posts", "sortByFactiii", "mediaSearch"]),
17033
+ postModeType: external_exports.enum(["home", "explore"]),
17034
+ muteVideo: external_exports.boolean(),
17035
+ theme: external_exports.enum(["dark", "light"]),
17036
+ selectedFactiiis: external_exports.array(external_exports.number()),
17037
+ dataFilter: external_exports.string(),
17038
+ mediaSearch: external_exports.string()
17039
+ });
17040
+ var localPreferenceUrlParamsSchema = external_exports.object({
17041
+ postFilterType: external_exports.enum(["trending", "new", "popular"]).optional(),
17042
+ postSortBy: external_exports.nativeEnum(postDataSort).optional(),
17043
+ filterBot: external_exports.preprocess((val) => val === "true", external_exports.boolean()).optional(),
17044
+ showBoards: external_exports.preprocess((val) => val === "true", external_exports.boolean()).optional(),
17045
+ displayMode: external_exports.enum(["data", "posts", "sortByFactiii", "mediaSearch"]).optional(),
17046
+ postModeType: external_exports.enum(["home", "explore"]).optional(),
17047
+ selectedFactiiis: external_exports.preprocess((val) => {
17048
+ if (!val || typeof val !== "string") return [];
17049
+ return val.split(",").map(Number).filter((id) => !isNaN(id));
17050
+ }, external_exports.array(external_exports.number())).optional()
17051
+ });
17052
+ var productSchema = external_exports.object({
17053
+ id: external_exports.number(),
17054
+ title: external_exports.string(),
17055
+ description: external_exports.string().nullable(),
17056
+ price: external_exports.number(),
17057
+ createdAt: external_exports.date(),
17058
+ active: external_exports.boolean(),
17059
+ originalInventory: external_exports.number().optional().nullable().transform((val) => val === void 0 ? null : val),
17060
+ inventory: external_exports.number(),
17061
+ discount: external_exports.number(),
17062
+ type: external_exports.enum(["TRON", "MONTHLY_SUBSCRIPTION", "FOUNDERS_TOKEN", "DONATION"]),
17063
+ appStoreProductId: external_exports.string().nullable(),
17064
+ playStoreProductId: external_exports.string().nullable(),
17065
+ stripePriceId: external_exports.string().nullable(),
17066
+ spaceSlug: external_exports.string(),
17067
+ images: external_exports.array(external_exports.string()),
17068
+ // Trons granted to the buyer on purchase + premium days granted. Only meaningful
17069
+ // for TRON and FOUNDERS_TOKEN; other product types ignore these. Optional so the
17070
+ // admin form can omit them when editing subscription/donation rows.
17071
+ tronQuantity: external_exports.number().int().min(0).optional(),
17072
+ tronPremiumDays: external_exports.number().int().min(0).optional()
17073
+ });
17074
+ var adminTransferSpaceOwnershipSchema = external_exports.object({
17075
+ spaceId: external_exports.number(),
17076
+ newOwnerUserId: external_exports.number()
17077
+ });
17078
+ var baseSchema = {
17079
+ cursor: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
17080
+ type: external_exports.enum(["new", "trending", "popular", "board"]).optional(),
17081
+ filterBots: external_exports.boolean().optional(),
17082
+ // Omitted = show; hiding new accounts is opt-in via the quick filter.
17083
+ filterNewAccounts: external_exports.boolean().optional(),
17084
+ showBoards: external_exports.boolean().optional(),
17085
+ factiiiIds: external_exports.number().array().optional(),
17086
+ strictFactiiiIds: external_exports.number().array().optional(),
17087
+ //used for getting posts by relatedfactiiicountgroup
17088
+ dataFilter: external_exports.string().optional(),
17089
+ start: external_exports.date().optional(),
17090
+ end: external_exports.date().optional()
17091
+ };
17092
+ var postFeedInputSchema = external_exports.discriminatedUnion("source", [
17093
+ external_exports.object({
17094
+ source: external_exports.literal("home"),
17095
+ ...baseSchema
17096
+ }),
17097
+ external_exports.object({
17098
+ source: external_exports.literal("explore"),
17099
+ ...baseSchema
17100
+ }),
17101
+ external_exports.object({
17102
+ source: external_exports.literal("user"),
17103
+ username: external_exports.string(),
17104
+ ...baseSchema
17105
+ }),
17106
+ external_exports.object({
17107
+ source: external_exports.literal("space"),
17108
+ spaceSlug: external_exports.string(),
17109
+ spaceRenderType: external_exports.enum(["USER_FACING", "SPACE_FACING", "BOTH"]).optional(),
17110
+ ...baseSchema
17111
+ }),
17112
+ external_exports.object({
17113
+ source: external_exports.literal("saved"),
17114
+ ...baseSchema
17115
+ }),
17116
+ external_exports.object({
17117
+ source: external_exports.literal("factiiis"),
17118
+ ...baseSchema
17119
+ }),
17120
+ // Every anonymous post, author masked. Backs /user/Anonymous%20User, which is
17121
+ // where anonymous posts' author links point.
17122
+ external_exports.object({
17123
+ source: external_exports.literal("anonymous"),
17124
+ ...baseSchema
17125
+ }),
17126
+ external_exports.object({
17127
+ source: external_exports.literal("search"),
17128
+ query: external_exports.string().optional(),
17129
+ spaceRenderType: external_exports.enum(["USER_FACING", "SPACE_FACING", "BOTH"]).optional(),
17130
+ userIds: external_exports.number().array().optional(),
17131
+ spaceIds: external_exports.number().array().optional(),
17132
+ ...baseSchema
17133
+ })
17134
+ ]);
17135
+ var donateTronsSchema = external_exports.object({
17136
+ amount: external_exports.number().int().positive()
17137
+ });
17138
+ var communityPoolModeSchema = external_exports.enum(["OFF", "WHITELIST", "OPEN"]);
17139
+ var communityPoolConfigSchema = external_exports.object({
17140
+ mode: communityPoolModeSchema,
17141
+ userCapPer4h: external_exports.number().int().min(0),
17142
+ poolCapPer4h: external_exports.number().int().min(0)
17143
+ });
17144
+ var crossPostPlatformEnum = external_exports.enum([
17145
+ "TIKTOK",
17146
+ "YOUTUBE",
17147
+ "INSTAGRAM",
17148
+ "FACEBOOK",
17149
+ "TWITTER",
17150
+ "LINKEDIN",
17151
+ "REDDIT"
17152
+ ]);
17153
+ var crossPostSchema = external_exports.object({
17154
+ postId: external_exports.number().int().positive(),
17155
+ platforms: external_exports.array(crossPostPlatformEnum).min(1)
17156
+ });
17157
+ var tiktokPostOptionsSchema = external_exports.object({
17158
+ postId: external_exports.number().int().positive(),
17159
+ privacyLevel: external_exports.enum([
17160
+ "PUBLIC_TO_EVERYONE",
17161
+ "MUTUAL_FOLLOW_FRIENDS",
17162
+ "FOLLOWER_OF_CREATOR",
17163
+ "SELF_ONLY"
17164
+ ]),
17165
+ disableComment: external_exports.boolean().default(false),
17166
+ disableDuet: external_exports.boolean().default(false),
17167
+ disableStitch: external_exports.boolean().default(false),
17168
+ videoCoverTimestampMs: external_exports.number().int().min(0).optional(),
17169
+ isAiGenerated: external_exports.boolean().default(false)
17170
+ });
17171
+ var crossPostSubmitSchema = external_exports.object({
17172
+ postId: external_exports.string(),
17173
+ platforms: external_exports.array(crossPostPlatformEnum).min(1).max(5)
17174
+ });
17175
+ var apiCostUpdateSchema = external_exports.object({
17176
+ platform: crossPostPlatformEnum,
17177
+ readCostTrons: external_exports.number().int().min(0),
17178
+ pushCostTrons: external_exports.number().int().min(0),
17179
+ costSource: external_exports.enum(["manual", "api"]),
17180
+ notes: external_exports.string().max(500).optional()
17181
+ });
17182
+ var startSessionSchema = external_exports.object({
17183
+ isDaily: external_exports.boolean().default(false)
17184
+ });
17185
+ var submitAnswerSchema = external_exports.object({
17186
+ sessionId: external_exports.number().int(),
17187
+ postId: external_exports.number().int(),
17188
+ answeredFact: external_exports.boolean(),
17189
+ timeMs: external_exports.number().int().min(0).max(15e3)
17190
+ });
17191
+ var endSessionSchema = external_exports.object({
17192
+ sessionId: external_exports.number().int()
17193
+ });
17194
+ var getLeaderboardSchema = external_exports.object({
17195
+ period: external_exports.enum(["daily", "weekly", "allTime"]),
17196
+ limit: external_exports.number().int().min(1).max(50).default(50)
17197
+ });
17198
+ var getSessionResultSchema = external_exports.object({
17199
+ sessionId: external_exports.number().int()
17200
+ });
17201
+ var forgeElementName = external_exports.string().trim().min(1).max(24);
17202
+ var forgeCombineSchema = external_exports.object({
17203
+ a: forgeElementName,
17204
+ b: forgeElementName
17205
+ });
17206
+ var forgeLeaderboardSchema = external_exports.object({
17207
+ limit: external_exports.number().int().min(1).max(50).default(25)
17208
+ });
17209
+ var arcadeGameSlug = external_exports.enum([
17210
+ "circle",
17211
+ "password",
17212
+ "forge",
17213
+ "spend",
17214
+ "dilemmas",
17215
+ "clicker",
17216
+ "deep",
17217
+ "robot",
17218
+ "space"
17219
+ ]);
17220
+ var recordGameStatSchema = external_exports.object({
17221
+ game: arcadeGameSlug,
17222
+ /** Canonical sortable score for that game; direction is decided client-side. */
17223
+ best: external_exports.number().finite(),
17224
+ /** Per-game extras, kept small — this lands in a JSON column. */
17225
+ detail: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number()])).optional()
17226
+ });
17227
+ var dilemmaVoteSchema = external_exports.object({
17228
+ dilemmaId: external_exports.number().int().min(1).max(200),
17229
+ pulled: external_exports.boolean()
17230
+ });
17231
+ var SKILL_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
17232
+ var skillNameSchema = external_exports.string().min(1).max(64).regex(SKILL_NAME_REGEX, {
17233
+ message: "Use only letters, numbers, dots, dashes and underscores"
17234
+ });
17235
+ var skillFrontmatterSchema = external_exports.object({
17236
+ name: skillNameSchema,
17237
+ description: external_exports.string().min(1, { message: "Description is required" }),
17238
+ // Both CLIs accept a comma-separated string or a YAML list; normalised to
17239
+ // a list on parse and written back as a list.
17240
+ "allowed-tools": external_exports.array(external_exports.string()).optional(),
17241
+ model: external_exports.string().optional(),
17242
+ license: external_exports.string().optional(),
17243
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
17244
+ "disable-model-invocation": external_exports.literal(false).optional()
17245
+ }).strict();
17246
+ var skillAgentSchema = external_exports.enum(["claude", "codex"]);
17247
+ var skillScopeSchema = external_exports.enum(["global", "repo"]);
17248
+
17249
+ // ../../shared/all/helpers/board-agent-core/skill-format.ts
17250
+ var EMPTY_FRONTMATTER = { name: "", description: "" };
17251
+ function splitKey(line) {
17252
+ const at = line.indexOf(":");
17253
+ if (at < 1) return null;
17254
+ return { key: line.slice(0, at).trim(), value: line.slice(at + 1).trim() };
17255
+ }
17256
+ function stripQuotes(value2) {
17257
+ const quoted = value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'");
17258
+ return quoted && value2.length > 1 ? value2.slice(1, -1) : value2;
17259
+ }
17260
+ function parseSkill(raw) {
17261
+ const text = raw.replace(/^/, "");
17262
+ if (!text.startsWith("---")) {
17263
+ return {
17264
+ frontmatter: EMPTY_FRONTMATTER,
17265
+ body: text,
17266
+ errors: ["Must start with YAML frontmatter (---)."]
17267
+ };
17268
+ }
17269
+ const lines = text.split("\n");
17270
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
17271
+ if (end === -1) {
17272
+ return {
17273
+ frontmatter: EMPTY_FRONTMATTER,
17274
+ body: text,
17275
+ errors: ["Frontmatter is never closed (missing the second ---)."]
17276
+ };
17277
+ }
17278
+ const errors = [];
17279
+ const fields = {};
17280
+ let pendingKey = "";
17281
+ let list = [];
17282
+ let map = {};
17283
+ const flush = () => {
17284
+ if (pendingKey) {
17285
+ if (list.length) fields[pendingKey] = list;
17286
+ else if (Object.keys(map).length) fields[pendingKey] = map;
17287
+ }
17288
+ pendingKey = "";
17289
+ list = [];
17290
+ map = {};
17291
+ };
17292
+ for (const line of lines.slice(1, end)) {
17293
+ if (!line.trim() || line.trim().startsWith("#")) continue;
17294
+ const indented = /^\s/.test(line);
17295
+ if (indented && pendingKey) {
17296
+ if (line.trim().startsWith("- ")) {
17297
+ list.push(stripQuotes(line.trim().slice(2).trim()));
17298
+ } else {
17299
+ const nested = splitKey(line);
17300
+ if (nested) map[nested.key] = stripQuotes(nested.value);
17301
+ else errors.push(`Cannot read indented line: ${line.trim()}`);
17302
+ }
17303
+ continue;
17304
+ }
17305
+ if (indented) {
17306
+ errors.push(`Cannot read indented line: ${line.trim()}`);
17307
+ continue;
17308
+ }
17309
+ flush();
17310
+ const pair = splitKey(line);
17311
+ if (!pair) {
17312
+ errors.push(`Cannot read line: ${line.trim()}`);
17313
+ continue;
17314
+ }
17315
+ if (pair.value === "") {
17316
+ pendingKey = pair.key;
17317
+ continue;
17318
+ }
17319
+ fields[pair.key] = stripQuotes(pair.value);
17320
+ }
17321
+ flush();
17322
+ const tools = fields["allowed-tools"];
17323
+ if (typeof tools === "string") {
17324
+ fields["allowed-tools"] = tools.split(",").map((tool) => tool.trim()).filter(Boolean);
17325
+ }
17326
+ if (fields["disable-model-invocation"] === "true") {
17327
+ errors.push("disable-model-invocation must be false.");
17328
+ delete fields["disable-model-invocation"];
17329
+ }
17330
+ if (fields["disable-model-invocation"] === "false") {
17331
+ fields["disable-model-invocation"] = false;
17332
+ }
17333
+ const body = lines.slice(end + 1).join("\n").replace(/^\n/, "");
17334
+ const parsed = skillFrontmatterSchema.safeParse(fields);
17335
+ if (!parsed.success) {
17336
+ for (const issue of parsed.error.issues) {
17337
+ errors.push(`${issue.path.join(".") || "frontmatter"}: ${issue.message}`);
17338
+ }
17339
+ return {
17340
+ frontmatter: {
17341
+ name: typeof fields.name === "string" ? fields.name : "",
17342
+ description: typeof fields.description === "string" ? fields.description : ""
17343
+ },
17344
+ body,
17345
+ errors
17346
+ };
17347
+ }
17348
+ return { frontmatter: parsed.data, body, errors };
17349
+ }
17350
+ function resolveSkillDir(root, name) {
17351
+ if (!SKILL_NAME_REGEX.test(name) || name === "." || name === "..") {
17352
+ throw new Error(`Invalid skill name: ${name}`);
17353
+ }
17354
+ return `${root.replace(/[/\\]+$/, "")}/${name}`;
17355
+ }
17356
+
17357
+ // ../../shared/all/helpers/board-agent-core/host-skills.ts
17358
+ var AGENTS_ROOT = ".agents/skills";
17359
+ var CLAUDE_ROOT = ".claude/skills";
17360
+ var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
17361
+ function globalAgentsRoot() {
17362
+ return import_path8.default.join(import_os2.default.homedir(), ".agents", "skills");
17363
+ }
17364
+ var AGENTS_LINK_TARGET = "../.agents/skills";
17365
+ var BRANCH_PREFIX = "skill/";
17366
+ var SYMLINK_MODE = "120000";
17367
+ function summarize(raw, base) {
17368
+ const { frontmatter, errors } = parseSkill(raw);
17369
+ return { ...base, description: frontmatter.description, errors };
17370
+ }
17371
+ async function rootLinkState(root) {
17372
+ const linkPath = import_path8.default.join(import_os2.default.homedir(), ...root.split("/"));
17373
+ const stat = await import_promises5.default.lstat(linkPath).catch(() => null);
17374
+ if (!stat) return "missing";
17375
+ if (!stat.isSymbolicLink()) return "blocked";
17376
+ const target = await import_promises5.default.readlink(linkPath).catch(() => "");
17377
+ return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
17378
+ }
17379
+ async function globalLinkState() {
17380
+ const states = await Promise.all(CLI_ROOTS.map(rootLinkState));
17381
+ if (states.includes("blocked")) return "blocked";
17382
+ return states.includes("missing") ? "missing" : "linked";
17383
+ }
17384
+ async function listGlobal() {
17385
+ const root = globalAgentsRoot();
17386
+ const entries = await import_promises5.default.readdir(root, { withFileTypes: true }).catch(() => []);
17387
+ const out = [];
17388
+ for (const entry of entries) {
17389
+ if (entry.isFile()) continue;
17390
+ const dir = import_path8.default.join(root, entry.name);
17391
+ const raw = await import_promises5.default.readFile(import_path8.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
17392
+ if (!raw) continue;
17393
+ out.push(
17394
+ summarize(raw, {
17395
+ name: entry.name,
17396
+ scope: "global",
17397
+ dir,
17398
+ status: "live",
17399
+ pendingBranch: ""
17400
+ })
17401
+ );
17402
+ }
17403
+ return out.sort((a, b) => a.name.localeCompare(b.name));
17404
+ }
17405
+ function strayRepoSkills(liveBlobs) {
17406
+ return [...liveBlobs.keys()].filter((key) => CLI_ROOTS.some((root) => key.startsWith(`${root}/`))).sort();
17407
+ }
17408
+ async function treeAt(repo, ref) {
17409
+ const raw = await hostGit(
17410
+ repo,
17411
+ "ls-tree",
17412
+ "-r",
17413
+ ref,
17414
+ "--",
17415
+ AGENTS_ROOT,
17416
+ ...CLI_ROOTS
17417
+ ).catch(() => "");
17418
+ return raw.split("\n").filter(Boolean).map((line) => {
17419
+ const [meta, filePath] = line.split(" ");
17420
+ const [mode, , sha] = meta.split(/\s+/);
17421
+ return { mode, sha, filePath };
17422
+ }).filter((entry) => Boolean(entry.filePath));
17423
+ }
17424
+ function skillBlobs(entries) {
17425
+ const blobs = /* @__PURE__ */ new Map();
17426
+ for (const entry of entries) {
17427
+ const match = /^(.+\/skills)\/([^/]+)\/SKILL\.md$/.exec(entry.filePath);
17428
+ if (match) blobs.set(`${match[1]}/${match[2]}`, entry.sha);
17429
+ }
17430
+ return blobs;
17431
+ }
17432
+ async function skillLinks(repo, entries) {
17433
+ const links = /* @__PURE__ */ new Map();
17434
+ const linkEntries = entries.filter(
17435
+ (entry) => entry.mode === SYMLINK_MODE && /^.+\/skills(\/[^/]+)?$/.test(entry.filePath)
17436
+ );
17437
+ const targets = await gitCatFileBatch(
17438
+ repo,
17439
+ linkEntries.map((entry) => entry.sha)
17440
+ );
17441
+ for (const entry of linkEntries) {
17442
+ const target = targets.get(entry.sha)?.trim();
17443
+ if (target) links.set(entry.filePath, target);
17444
+ }
17445
+ return links;
17446
+ }
17447
+ function resolveRepoSkills(liveBlobs, pending) {
17448
+ const keys = /* @__PURE__ */ new Set();
17449
+ for (const key of liveBlobs.keys()) {
17450
+ if (key.startsWith(`${AGENTS_ROOT}/`)) keys.add(key);
17451
+ }
17452
+ for (const entry of pending) {
17453
+ for (const key of entry.blobs.keys()) {
17454
+ if (key.startsWith(`${AGENTS_ROOT}/`)) keys.add(key);
17455
+ }
17456
+ }
17457
+ const out = [];
17458
+ for (const key of [...keys].sort()) {
17459
+ const name = key.slice(AGENTS_ROOT.length + 1);
17460
+ const live = liveBlobs.get(key) || "";
17461
+ const onBranch = pending.find((entry) => {
17462
+ const sha = entry.blobs.get(key);
17463
+ return Boolean(sha) && sha !== live;
17464
+ });
17465
+ const status = live ? onBranch ? "pending-edit" : "live" : "pending-new";
17466
+ out.push({
17467
+ name,
17468
+ dir: key,
17469
+ status,
17470
+ pendingBranch: status === "live" ? "" : onBranch?.branch || "",
17471
+ sha: live || onBranch?.blobs.get(key) || ""
17472
+ });
17473
+ }
17474
+ return out;
17475
+ }
17476
+ async function repoSkills(ctx) {
17477
+ const skills = resolveRepoSkills(ctx.liveBlobs, ctx.pending);
17478
+ const cache2 = /* @__PURE__ */ new Map();
17479
+ const missing = skills.map((skill) => skill.sha).filter((sha) => sha && !cache2.has(sha));
17480
+ for (const [sha, text] of await gitCatFileBatch(ctx.repo, missing)) {
17481
+ cache2.set(sha, text);
17482
+ }
17483
+ return skills.map(
17484
+ (skill) => summarize(cache2.get(skill.sha) ?? "", {
17485
+ name: skill.name,
17486
+ scope: "repo",
17487
+ dir: skill.dir,
17488
+ status: skill.status,
17489
+ pendingBranch: skill.pendingBranch
17490
+ })
17491
+ );
17492
+ }
17493
+ async function pendingBranches(repo, mainBranch) {
17494
+ const refs = await hostGit(
17495
+ repo,
17496
+ "for-each-ref",
17497
+ "--sort=-committerdate",
17498
+ "--format=%(refname:short)",
17499
+ `refs/remotes/origin/${BRANCH_PREFIX}*`
17500
+ ).catch(() => "");
17501
+ const branches = await Promise.all(
17502
+ refs.split("\n").filter(Boolean).map(async (ref) => {
17503
+ const merged = await hostGit(
17504
+ repo,
17505
+ "merge-base",
17506
+ "--is-ancestor",
17507
+ ref,
17508
+ `origin/${mainBranch}`
17509
+ ).then(
17510
+ () => true,
17511
+ () => false
17512
+ );
17513
+ if (merged) return null;
17514
+ return {
17515
+ branch: ref.replace(/^origin\//, ""),
17516
+ blobs: skillBlobs(await treeAt(repo, ref))
17517
+ };
17518
+ })
17519
+ );
17520
+ return branches.filter((entry) => entry !== null);
17521
+ }
17522
+ function requireRepo(opts) {
17523
+ if (!opts.repoUrl || !opts.githubToken) {
17524
+ throw new Error("Set the repository URL and GitHub token first.");
17525
+ }
17526
+ return {
17527
+ spaceDir: opts.spaceDir,
17528
+ repoUrl: opts.repoUrl,
17529
+ mainBranch: opts.mainBranch,
17530
+ githubToken: opts.githubToken
17531
+ };
17532
+ }
17533
+ async function ensureSkillsRepo(opts) {
17534
+ const { spaceDir, repoUrl, mainBranch, githubToken } = requireRepo(opts);
17535
+ const paths = hostSpacePaths(spaceDir);
17536
+ const branch = (mainBranch || "main").replace(/[^\w./-]/g, "");
17537
+ const url2 = buildCloneUrl(repoUrl, githubToken);
17538
+ try {
17539
+ await hostGit(paths.repo, "rev-parse", "--git-dir");
17540
+ await hostGit(paths.repo, "remote", "set-url", "origin", url2);
17541
+ } catch {
17542
+ await hostGit(paths.root, "clone", "--branch", branch, url2, paths.repo);
17543
+ }
17544
+ await hostGit(
17545
+ paths.repo,
17546
+ "fetch",
17547
+ "origin",
17548
+ branch,
17549
+ `+refs/heads/${BRANCH_PREFIX}*:refs/remotes/origin/${BRANCH_PREFIX}*`
17550
+ );
17551
+ return { repo: paths.repo, branch };
17552
+ }
17553
+ async function ensureSkillsWorktree(spaceDir, branch) {
17554
+ const { repo, skills: worktree } = hostSpacePaths(spaceDir);
17555
+ try {
17556
+ await hostGit(worktree, "rev-parse", "--git-dir");
17557
+ } catch {
17558
+ await hostGit(repo, "worktree", "prune").catch(() => {
17559
+ });
17560
+ await import_promises5.default.rm(worktree, { recursive: true, force: true });
17561
+ await hostGit(
17562
+ repo,
17563
+ "worktree",
17564
+ "add",
17565
+ "--detach",
17566
+ worktree,
17567
+ `origin/${branch}`
17568
+ );
17569
+ }
17570
+ return worktree;
17571
+ }
17572
+ async function repoContext(opts) {
17573
+ const { repo, branch } = await ensureSkillsRepo(opts);
17574
+ const liveTree = await treeAt(repo, `origin/${branch}`);
17575
+ const [links, pending] = await Promise.all([
17576
+ skillLinks(repo, liveTree),
17577
+ pendingBranches(repo, branch)
17578
+ ]);
17579
+ return {
17580
+ repo,
17581
+ branch,
17582
+ liveTree,
17583
+ liveBlobs: skillBlobs(liveTree),
17584
+ links,
17585
+ pending
17586
+ };
17587
+ }
17588
+ function findRepoSkill(ctx, name) {
17589
+ return resolveRepoSkills(ctx.liveBlobs, ctx.pending).find(
17590
+ (skill) => skill.name === name
17591
+ ) ?? null;
17592
+ }
17593
+ var workspaceQueues = /* @__PURE__ */ new Map();
17594
+ function withWorkspace(spaceDir, run2) {
17595
+ const next = (workspaceQueues.get(spaceDir) ?? Promise.resolve()).then(
17596
+ run2,
17597
+ run2
17598
+ );
17599
+ workspaceQueues.set(
17600
+ spaceDir,
17601
+ next.catch(() => {
17602
+ })
17603
+ );
17604
+ return next;
17605
+ }
17606
+ function repoLinkState(ctx) {
17607
+ const target = ctx.links.get(CLAUDE_ROOT);
17608
+ if (target) return target === AGENTS_LINK_TARGET ? "linked" : "missing";
17609
+ return [...ctx.liveBlobs.keys()].some(
17610
+ (key) => key.startsWith(`${CLAUDE_ROOT}/`)
17611
+ ) ? "blocked" : "missing";
17612
+ }
17613
+ async function listSkillsImpl(opts) {
17614
+ const [global2, globalLink] = await Promise.all([
17615
+ listGlobal(),
17616
+ globalLinkState()
17617
+ ]);
17618
+ if (!opts.repoUrl || !opts.githubToken) {
17619
+ return {
17620
+ global: global2,
17621
+ repo: [],
17622
+ globalLink,
17623
+ repoLink: "linked",
17624
+ repoBranch: "",
17625
+ repoAvailable: false
17626
+ };
17627
+ }
17628
+ const ctx = await repoContext(opts);
17629
+ const repo = await repoSkills(ctx);
17630
+ return {
17631
+ global: global2,
17632
+ repo,
17633
+ globalLink,
17634
+ repoLink: repoLinkState(ctx),
17635
+ repoBranch: ctx.branch,
17636
+ repoAvailable: true
17637
+ };
17638
+ }
17639
+ async function bundleFilesOnDisk(dir) {
17640
+ const out = [];
17641
+ const walk = async (current, prefix, depth) => {
17642
+ if (depth > 3 || out.length >= 200) return;
17643
+ const entries = await import_promises5.default.readdir(current, { withFileTypes: true }).catch(() => []);
17644
+ for (const entry of entries) {
17645
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
17646
+ if (entry.isDirectory()) {
17647
+ await walk(import_path8.default.join(current, entry.name), rel, depth + 1);
17648
+ } else if (rel !== "SKILL.md") {
17649
+ out.push(rel);
17650
+ }
17651
+ }
17652
+ };
17653
+ await walk(dir, "", 0);
17654
+ return out.sort();
17655
+ }
17656
+ async function readSkillImpl(opts) {
17657
+ if (opts.scope === "global") {
17658
+ const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
17659
+ const content2 = await import_promises5.default.readFile(import_path8.default.join(dir, "SKILL.md"), "utf-8");
17660
+ const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
17661
+ return {
17662
+ name: opts.name,
17663
+ scope: "global",
17664
+ content: content2,
17665
+ frontmatter: frontmatter2,
17666
+ errors: errors2,
17667
+ bundleFiles: await bundleFilesOnDisk(dir),
17668
+ status: "live",
17669
+ pendingBranch: ""
17670
+ };
17671
+ }
17672
+ const ctx = await repoContext(opts);
17673
+ const skill = findRepoSkill(ctx, opts.name);
17674
+ if (!skill) throw new Error(`No repo skill named ${opts.name}.`);
17675
+ const draft = skill.pendingBranch ? ctx.pending.find((entry) => entry.branch === skill.pendingBranch)?.blobs.get(skill.dir) : "";
17676
+ const content = await hostGit(ctx.repo, "cat-file", "-p", draft || skill.sha);
17677
+ const { frontmatter, errors } = parseSkill(content);
17678
+ const tree = draft ? await treeAt(ctx.repo, `origin/${skill.pendingBranch}`) : ctx.liveTree;
17679
+ const bundleFiles = tree.filter(
17680
+ (entry) => entry.filePath.startsWith(`${skill.dir}/`) && entry.filePath !== `${skill.dir}/SKILL.md`
17681
+ ).map((entry) => entry.filePath.slice(skill.dir.length + 1)).sort();
17682
+ return {
17683
+ name: skill.name,
17684
+ scope: "repo",
17685
+ content,
17686
+ frontmatter,
17687
+ errors,
17688
+ bundleFiles,
17689
+ status: skill.status,
17690
+ pendingBranch: skill.pendingBranch
17691
+ };
17692
+ }
17693
+ async function moveIntoAgents(from, to) {
17694
+ if (await import_promises5.default.access(to).then(
17695
+ () => true,
17696
+ () => false
17697
+ )) {
17698
+ throw new Error(
17699
+ `${import_path8.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
17700
+ );
17701
+ }
17702
+ await import_promises5.default.mkdir(import_path8.default.dirname(to), { recursive: true });
17703
+ try {
17704
+ await import_promises5.default.rename(from, to);
17705
+ } catch {
17706
+ await import_promises5.default.cp(from, to, { recursive: true });
17707
+ await import_promises5.default.rm(from, { recursive: true, force: true });
17708
+ }
17709
+ }
17710
+ async function migrateGlobal() {
17711
+ const moved = [];
17712
+ for (const root of CLI_ROOTS) {
17713
+ const dir = import_path8.default.join(import_os2.default.homedir(), ...root.split("/"));
17714
+ const stat = await import_promises5.default.lstat(dir).catch(() => null);
17715
+ if (!stat || stat.isSymbolicLink()) continue;
17716
+ const entries = await import_promises5.default.readdir(dir, { withFileTypes: true }).catch(() => []);
17717
+ for (const entry of entries) {
17718
+ if (entry.isFile() || entry.isSymbolicLink()) continue;
17719
+ const src = import_path8.default.join(dir, entry.name);
17720
+ if (!await import_promises5.default.access(import_path8.default.join(src, "SKILL.md")).then(
17721
+ () => true,
17722
+ () => false
17723
+ )) {
17724
+ continue;
17725
+ }
17726
+ await moveIntoAgents(src, import_path8.default.join(globalAgentsRoot(), entry.name));
17727
+ moved.push(entry.name);
17728
+ }
17729
+ await import_promises5.default.rmdir(dir).catch(() => {
17730
+ });
17731
+ }
17732
+ for (const root of CLI_ROOTS) {
17733
+ const linkPath = import_path8.default.join(import_os2.default.homedir(), ...root.split("/"));
17734
+ const after = await import_promises5.default.lstat(linkPath).catch(() => null);
17735
+ if (after?.isSymbolicLink()) await import_promises5.default.rm(linkPath, { force: true });
17736
+ else if (after) {
17737
+ throw new Error(
17738
+ `~/${root} still has files that are not skills. Empty it, then migrate.`
17739
+ );
17740
+ }
17741
+ await import_promises5.default.mkdir(import_path8.default.dirname(linkPath), { recursive: true });
17742
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
17743
+ }
17744
+ return moved;
17745
+ }
17746
+ async function migrateSkillsImpl(opts) {
17747
+ if (opts.scope === "global") {
17748
+ return { branch: "", moved: await migrateGlobal() };
17749
+ }
17750
+ const ctx = await repoContext(opts);
17751
+ const strays = strayRepoSkills(ctx.liveBlobs);
17752
+ const linked = ctx.links.get(CLAUDE_ROOT);
17753
+ if (!strays.length && linked === AGENTS_LINK_TARGET) {
17754
+ return { branch: "", moved: [] };
17755
+ }
17756
+ const { branch } = ctx;
17757
+ const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
17758
+ const target = `${BRANCH_PREFIX}migrate`;
17759
+ await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
17760
+ await hostGit(workspace, "clean", "-fd");
17761
+ if (opts.gitName) {
17762
+ await hostGit(workspace, "config", "user.name", opts.gitName);
17763
+ }
17764
+ if (opts.gitEmail) {
17765
+ await hostGit(workspace, "config", "user.email", opts.gitEmail);
17766
+ }
17767
+ const moved = [];
17768
+ for (const stray of strays) {
17769
+ const name = stray.slice(stray.lastIndexOf("/") + 1);
17770
+ const dest = `${AGENTS_ROOT}/${name}`;
17771
+ if (ctx.liveBlobs.has(dest)) {
17772
+ throw new Error(
17773
+ `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
17774
+ );
17775
+ }
17776
+ await import_promises5.default.mkdir(import_path8.default.join(workspace, AGENTS_ROOT), { recursive: true });
17777
+ await hostGit(workspace, "mv", stray, dest);
17778
+ moved.push(name);
17779
+ }
17780
+ const linkPath = import_path8.default.join(workspace, CLAUDE_ROOT);
17781
+ await import_promises5.default.rm(linkPath, { recursive: true, force: true });
17782
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
17783
+ await hostGit(workspace, "add", "-A");
17784
+ const dirty = await hostGit(workspace, "status", "--porcelain");
17785
+ if (!dirty.trim()) return { branch: "", moved: [] };
17786
+ await hostGit(
17787
+ workspace,
17788
+ "commit",
17789
+ "-m",
17790
+ `Migrate ${moved.length} skill(s) to ${AGENTS_ROOT} and link ${CLAUDE_ROOT}`
17791
+ );
17792
+ await hostGit(workspace, "push", "origin", target);
17793
+ return { branch: target, moved };
17794
+ }
17795
+ function assertWritable(name, content) {
17796
+ const parsed = parseSkill(content);
17797
+ if (parsed.errors.length) {
17798
+ throw new Error(`Fix the skill first: ${parsed.errors.join(" ")}`);
17799
+ }
17800
+ if (parsed.frontmatter.name !== name) {
17801
+ throw new Error(
17802
+ `Frontmatter name (${parsed.frontmatter.name}) must match the skill directory (${name}).`
17803
+ );
17804
+ }
17805
+ }
17806
+ async function writeGlobalSkill(opts) {
17807
+ assertWritable(opts.name, opts.content);
17808
+ const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
17809
+ await import_promises5.default.mkdir(dir, { recursive: true });
17810
+ await import_promises5.default.writeFile(import_path8.default.join(dir, "SKILL.md"), opts.content, "utf-8");
17811
+ }
17812
+ async function deleteGlobalSkill(opts) {
17813
+ const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
17814
+ await import_promises5.default.rm(dir, { recursive: true, force: true });
17815
+ }
17816
+ async function proposeRepoSkillImpl(opts) {
17817
+ assertWritable(opts.name, opts.content);
17818
+ if (!SKILL_NAME_REGEX.test(opts.name)) {
17819
+ throw new Error(`Invalid skill name: ${opts.name}`);
17820
+ }
17821
+ const ctx = await repoContext(opts);
17822
+ const known = findRepoSkill(ctx, opts.name);
17823
+ const dir = known?.dir || `${AGENTS_ROOT}/${opts.name}`;
17824
+ const { branch } = ctx;
17825
+ const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
17826
+ const target = `${BRANCH_PREFIX}${opts.name}`;
17827
+ const existing = await hostGit(
17828
+ workspace,
17829
+ "ls-remote",
17830
+ "--heads",
17831
+ "origin",
17832
+ target
17833
+ ).catch(() => "");
17834
+ if (existing.trim()) {
17835
+ await hostGit(
17836
+ workspace,
17837
+ "fetch",
17838
+ "origin",
17839
+ `+refs/heads/${target}:refs/remotes/origin/${target}`
17840
+ );
17841
+ await hostGit(workspace, "checkout", "-B", target, `origin/${target}`);
17842
+ } else {
17843
+ await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
17844
+ }
17845
+ await hostGit(workspace, "clean", "-fd");
17846
+ if (opts.gitName) {
17847
+ await hostGit(workspace, "config", "user.name", opts.gitName);
17848
+ }
17849
+ if (opts.gitEmail) {
17850
+ await hostGit(workspace, "config", "user.email", opts.gitEmail);
17851
+ }
17852
+ await import_promises5.default.mkdir(import_path8.default.join(workspace, dir), { recursive: true });
17853
+ await import_promises5.default.writeFile(
17854
+ import_path8.default.join(workspace, dir, "SKILL.md"),
17855
+ opts.content,
17856
+ "utf-8"
17857
+ );
17858
+ const dirty = await hostGit(workspace, "status", "--porcelain");
17859
+ if (!dirty.trim()) return { branch: target };
17860
+ await hostGit(workspace, "add", "-A");
17861
+ await hostGit(
17862
+ workspace,
17863
+ "commit",
17864
+ "-m",
17865
+ `${known ? "Update" : "Add"} skill: ${opts.name}`
17866
+ );
17867
+ await hostGit(workspace, "push", "origin", target);
17868
+ return { branch: target };
17869
+ }
17870
+ function listSkills(opts) {
17871
+ return withWorkspace(opts.spaceDir, () => listSkillsImpl(opts));
17872
+ }
17873
+ function readSkill(opts) {
17874
+ return withWorkspace(opts.spaceDir, () => readSkillImpl(opts));
17875
+ }
17876
+ function migrateSkills(opts) {
17877
+ return withWorkspace(opts.spaceDir, () => migrateSkillsImpl(opts));
17878
+ }
17879
+ function proposeRepoSkill(opts) {
17880
+ return withWorkspace(opts.spaceDir, () => proposeRepoSkillImpl(opts));
17881
+ }
17882
+
16624
17883
  // ../../shared/all/helpers/board-agent-core/onedrive-auth.ts
16625
17884
  var DEVICECODE_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
16626
17885
  var TOKEN_URL2 = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
@@ -17365,6 +18624,48 @@ var BoardAgentEngine = class _BoardAgentEngine {
17365
18624
  githubToken: config.githubToken
17366
18625
  });
17367
18626
  }
18627
+ // ── Agent skills (SKILL.md) ──
18628
+ // Repo opts are optional here, unlike deploy: the global roots are readable
18629
+ // with no repo configured, and listSkills reports repoAvailable: false.
18630
+ skillOpts() {
18631
+ const config = this.core.readConfig();
18632
+ return {
18633
+ spaceDir: this.core.spaceDir(),
18634
+ repoUrl: config.repoUrl,
18635
+ mainBranch: config.mainBranch,
18636
+ githubToken: config.githubToken
18637
+ };
18638
+ }
18639
+ skillsList() {
18640
+ return listSkills(this.skillOpts());
18641
+ }
18642
+ skillRead(payload) {
18643
+ return readSkill({ ...this.skillOpts(), ...payload });
18644
+ }
18645
+ skillWriteGlobal(payload) {
18646
+ return writeGlobalSkill(payload);
18647
+ }
18648
+ skillDeleteGlobal(payload) {
18649
+ return deleteGlobalSkill(payload);
18650
+ }
18651
+ skillMigrate(payload) {
18652
+ const config = this.core.readConfig();
18653
+ return migrateSkills({
18654
+ ...this.skillOpts(),
18655
+ ...payload,
18656
+ gitName: config.gitName,
18657
+ gitEmail: config.gitEmail
18658
+ });
18659
+ }
18660
+ skillProposeRepo(payload) {
18661
+ const config = this.core.readConfig();
18662
+ return proposeRepoSkill({
18663
+ ...this.skillOpts(),
18664
+ ...payload,
18665
+ gitName: config.gitName,
18666
+ gitEmail: config.gitEmail
18667
+ });
18668
+ }
17368
18669
  deployCreatorState() {
17369
18670
  return this.creatorState;
17370
18671
  }
@@ -17747,7 +19048,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
17747
19048
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
17748
19049
  secretsDirs() {
17749
19050
  const spaceDir = this.core.spaceDir();
17750
- return { configDir: (0, import_path8.dirname)(spaceDir), spaceDir };
19051
+ return { configDir: (0, import_path9.dirname)(spaceDir), spaceDir };
17751
19052
  }
17752
19053
  async deploySecretsStatus() {
17753
19054
  return {
@@ -18537,6 +19838,28 @@ ${c.content.trim() || "(no description)"}`
18537
19838
  return await this.bareWorkNames();
18538
19839
  case "deployStatus":
18539
19840
  return await this.deployStatus();
19841
+ case "skillsList":
19842
+ return await this.skillsList();
19843
+ case "skillRead":
19844
+ return await this.skillRead(
19845
+ payload
19846
+ );
19847
+ case "skillWriteGlobal":
19848
+ return await this.skillWriteGlobal(
19849
+ payload
19850
+ );
19851
+ case "skillDeleteGlobal":
19852
+ return await this.skillDeleteGlobal(
19853
+ payload
19854
+ );
19855
+ case "skillMigrate":
19856
+ return await this.skillMigrate(
19857
+ payload
19858
+ );
19859
+ case "skillProposeRepo":
19860
+ return await this.skillProposeRepo(
19861
+ payload
19862
+ );
18540
19863
  case "deployCreateSkill":
18541
19864
  return await this.deployCreateSkill(
18542
19865
  payload,
@@ -18748,9 +20071,9 @@ ${c.content.trim() || "(no description)"}`
18748
20071
  var import_child_process5 = require("child_process");
18749
20072
  var import_crypto3 = require("crypto");
18750
20073
  var import_fs3 = require("fs");
18751
- var import_promises5 = __toESM(require("fs/promises"));
20074
+ var import_promises6 = __toESM(require("fs/promises"));
18752
20075
  var import_net2 = require("net");
18753
- var import_path9 = require("path");
20076
+ var import_path10 = require("path");
18754
20077
  var import_util7 = require("util");
18755
20078
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
18756
20079
  var IndexService = class {
@@ -18809,7 +20132,7 @@ var IndexService = class {
18809
20132
  const log = await this.openLog();
18810
20133
  this.proc = (0, import_child_process5.spawn)(
18811
20134
  process.execPath,
18812
- [(0, import_path9.join)(this.opts.indexDir, "index.js")],
20135
+ [(0, import_path10.join)(this.opts.indexDir, "index.js")],
18813
20136
  {
18814
20137
  cwd: this.opts.indexDir,
18815
20138
  env: {
@@ -18837,8 +20160,8 @@ var IndexService = class {
18837
20160
  * opened (a missing log must never stop the service from starting). */
18838
20161
  async openLog() {
18839
20162
  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");
20163
+ await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true });
20164
+ return (0, import_fs3.openSync)((0, import_path10.join)(this.opts.dataDir, "index.log"), "a");
18842
20165
  } catch {
18843
20166
  return null;
18844
20167
  }
@@ -18847,10 +20170,10 @@ var IndexService = class {
18847
20170
  * platform needing node-gyp fails here rather than at query time. */
18848
20171
  async installDeps() {
18849
20172
  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")
20173
+ if ((0, import_fs3.existsSync)((0, import_path10.join)(dir, "node_modules"))) return;
20174
+ await import_promises6.default.copyFile(
20175
+ (0, import_path10.join)(dir, "image-package.json"),
20176
+ (0, import_path10.join)(dir, "package.json")
18854
20177
  );
18855
20178
  await execFileAsync4(
18856
20179
  "npm",
@@ -18862,15 +20185,15 @@ var IndexService = class {
18862
20185
  * CLIs' persistent MCP config, so a fresh port each start would leave every
18863
20186
  * registration pointing at a dead one. */
18864
20187
  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(() => ""));
20188
+ const file = (0, import_path10.join)(this.opts.dataDir, ".port");
20189
+ const saved = Number(await import_promises6.default.readFile(file, "utf-8").catch(() => ""));
18867
20190
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
18868
20191
  return saved;
18869
20192
  }
18870
20193
  const port = await freePort();
18871
- await import_promises5.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
20194
+ await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
18872
20195
  });
18873
- await import_promises5.default.writeFile(file, String(port), "utf-8").catch(() => {
20196
+ await import_promises6.default.writeFile(file, String(port), "utf-8").catch(() => {
18874
20197
  });
18875
20198
  return port;
18876
20199
  }
@@ -19038,24 +20361,24 @@ async function pairWithBrowser(serverUrl) {
19038
20361
 
19039
20362
  // src/config.ts
19040
20363
  var import_fs7 = __toESM(require("fs"));
19041
- var import_os4 = __toESM(require("os"));
19042
- var import_path14 = __toESM(require("path"));
20364
+ var import_os5 = __toESM(require("os"));
20365
+ var import_path15 = __toESM(require("path"));
19043
20366
 
19044
20367
  // src/secureFile.ts
19045
20368
  var import_crypto6 = __toESM(require("crypto"));
19046
20369
  var import_fs6 = __toESM(require("fs"));
19047
- var import_path13 = __toESM(require("path"));
20370
+ var import_path14 = __toESM(require("path"));
19048
20371
 
19049
20372
  // src/keychain.ts
19050
20373
  var import_child_process7 = require("child_process");
19051
20374
  var import_crypto4 = __toESM(require("crypto"));
19052
20375
  var import_fs4 = __toESM(require("fs"));
19053
- var import_os2 = __toESM(require("os"));
19054
- var import_path10 = __toESM(require("path"));
20376
+ var import_os3 = __toESM(require("os"));
20377
+ var import_path11 = __toESM(require("path"));
19055
20378
  var SERVICE = "factiii-runner";
19056
20379
  var ACCOUNT = "config-encryption-key";
19057
- var DPAPI_KEY_FILE = import_path10.default.join(
19058
- import_os2.default.homedir(),
20380
+ var DPAPI_KEY_FILE = import_path11.default.join(
20381
+ import_os3.default.homedir(),
19059
20382
  ".factiii-runner",
19060
20383
  "config-key.dpapi"
19061
20384
  );
@@ -19160,7 +20483,7 @@ function winWrite(key) {
19160
20483
  "-Command",
19161
20484
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
19162
20485
  ]);
19163
- import_fs4.default.mkdirSync(import_path10.default.dirname(DPAPI_KEY_FILE), { recursive: true });
20486
+ import_fs4.default.mkdirSync(import_path11.default.dirname(DPAPI_KEY_FILE), { recursive: true });
19164
20487
  import_fs4.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
19165
20488
  `, { mode: 384 });
19166
20489
  }
@@ -19324,21 +20647,21 @@ function keychainStatus() {
19324
20647
  // src/secureStore.ts
19325
20648
  var import_crypto5 = __toESM(require("crypto"));
19326
20649
  var import_fs5 = __toESM(require("fs"));
19327
- var import_path12 = __toESM(require("path"));
20650
+ var import_path13 = __toESM(require("path"));
19328
20651
 
19329
20652
  // 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");
20653
+ var import_os4 = __toESM(require("os"));
20654
+ var import_path12 = __toESM(require("path"));
20655
+ var CONFIG_DIR = import_path12.default.join(import_os4.default.homedir(), ".factiii-runner");
19333
20656
  function safeSlug(spaceSlug) {
19334
20657
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
19335
20658
  }
19336
20659
  function spaceDirPath(spaceSlug) {
19337
- return import_path11.default.join(CONFIG_DIR, safeSlug(spaceSlug));
20660
+ return import_path12.default.join(CONFIG_DIR, safeSlug(spaceSlug));
19338
20661
  }
19339
20662
 
19340
20663
  // src/secureStore.ts
19341
- var VAULT_KEY_FILE = import_path12.default.join(CONFIG_DIR, "vault-key.json");
20664
+ var VAULT_KEY_FILE = import_path13.default.join(CONFIG_DIR, "vault-key.json");
19342
20665
  var SCRYPT_N2 = 1 << 15;
19343
20666
  var MIN_PASSWORD_LENGTH = 8;
19344
20667
  var SecureStoreError = class extends Error {
@@ -19660,7 +20983,7 @@ function parseEnvelope(raw) {
19660
20983
  }
19661
20984
  }
19662
20985
  function atomicWrite(filePath, body) {
19663
- import_fs6.default.mkdirSync(import_path13.default.dirname(filePath), { recursive: true });
20986
+ import_fs6.default.mkdirSync(import_path14.default.dirname(filePath), { recursive: true });
19664
20987
  const tmp = `${filePath}.${process.pid}.tmp`;
19665
20988
  import_fs6.default.writeFileSync(tmp, body, { mode: 384 });
19666
20989
  import_fs6.default.renameSync(tmp, filePath);
@@ -19775,8 +21098,8 @@ function writeBootJson(filePath, value2) {
19775
21098
  }
19776
21099
 
19777
21100
  // 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");
21101
+ var CONFIG_DIR2 = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
21102
+ var CONFIG_FILE = import_path15.default.join(CONFIG_DIR2, "config.json");
19780
21103
  function readRunnerConfig() {
19781
21104
  return readBootJson(CONFIG_FILE);
19782
21105
  }
@@ -19948,7 +21271,7 @@ function makeChunkReassembler() {
19948
21271
  var import_crypto7 = require("crypto");
19949
21272
  var import_fs9 = __toESM(require("fs"));
19950
21273
  var import_node_datachannel = require("node-datachannel");
19951
- var import_path16 = __toESM(require("path"));
21274
+ var import_path17 = __toESM(require("path"));
19952
21275
 
19953
21276
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
19954
21277
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -21128,12 +22451,12 @@ function parse2(str) {
21128
22451
  uri.queryKey = queryKey(uri, uri["query"]);
21129
22452
  return uri;
21130
22453
  }
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) {
22454
+ function pathNames(obj, path17) {
22455
+ const regx = /\/{2,9}/g, names = path17.replace(regx, "/").split("/");
22456
+ if (path17.slice(0, 1) == "/" || path17.length === 0) {
21134
22457
  names.splice(0, 1);
21135
22458
  }
21136
- if (path16.slice(-1) == "/") {
22459
+ if (path17.slice(-1) == "/") {
21137
22460
  names.splice(names.length - 1, 1);
21138
22461
  }
21139
22462
  return names;
@@ -21752,7 +23075,7 @@ var protocol2 = Socket.protocol;
21752
23075
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
21753
23076
  var import_debug7 = __toESM(require_src(), 1);
21754
23077
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
21755
- function url(uri, path16 = "", loc) {
23078
+ function url(uri, path17 = "", loc) {
21756
23079
  let obj = uri;
21757
23080
  loc = loc || typeof location !== "undefined" && location;
21758
23081
  if (null == uri)
@@ -21786,7 +23109,7 @@ function url(uri, path16 = "", loc) {
21786
23109
  obj.path = obj.path || "/";
21787
23110
  const ipv6 = obj.host.indexOf(":") !== -1;
21788
23111
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
21789
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path16;
23112
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path17;
21790
23113
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
21791
23114
  return obj;
21792
23115
  }
@@ -23420,8 +24743,8 @@ function lookup(uri, opts) {
23420
24743
  const parsed = url(uri, opts.path || "/socket.io");
23421
24744
  const source = parsed.source;
23422
24745
  const id = parsed.id;
23423
- const path16 = parsed.path;
23424
- const sameNamespace = cache[id] && path16 in cache[id]["nsps"];
24746
+ const path17 = parsed.path;
24747
+ const sameNamespace = cache[id] && path17 in cache[id]["nsps"];
23425
24748
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
23426
24749
  let io;
23427
24750
  if (newConnection) {
@@ -23463,8 +24786,8 @@ function cardContextPath(target, postId) {
23463
24786
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
23464
24787
  return `${target.paths.root}/.cards/${safe}.md`;
23465
24788
  }
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.`;
24789
+ function cardContextPrompt(path17) {
24790
+ return `Read ${path17} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
23468
24791
  }
23469
24792
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
23470
24793
  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 +24952,10 @@ var TerminalManager = class {
23629
24952
 
23630
24953
  // src/agent-adapter.ts
23631
24954
  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");
24955
+ var import_path16 = __toESM(require("path"));
24956
+ var RUNNER_CONFIG_PATH = import_path16.default.join(CONFIG_DIR, "runner-config.json");
23634
24957
  function connectionPath(spaceSlug) {
23635
- return import_path15.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
24958
+ return import_path16.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
23636
24959
  }
23637
24960
  function writeJson(filePath, value2) {
23638
24961
  writeSecureJson(filePath, value2);
@@ -23666,7 +24989,7 @@ function migrateLegacyConfigs() {
23666
24989
  for (const name of legacy) {
23667
24990
  const slug = name.slice("space-".length, -".json".length);
23668
24991
  const parsed = readSecureJson(
23669
- import_path15.default.join(CONFIG_DIR, name)
24992
+ import_path16.default.join(CONFIG_DIR, name)
23670
24993
  );
23671
24994
  if (!parsed) continue;
23672
24995
  if (!file.defaultSlug) {
@@ -23691,7 +25014,7 @@ function encryptStrayPlaintextConfigs() {
23691
25014
  return;
23692
25015
  }
23693
25016
  for (const name of names) {
23694
- const filePath = import_path15.default.join(CONFIG_DIR, name);
25017
+ const filePath = import_path16.default.join(CONFIG_DIR, name);
23695
25018
  try {
23696
25019
  const raw = import_fs8.default.readFileSync(filePath, "utf-8");
23697
25020
  if (!raw.startsWith("{")) continue;
@@ -23820,7 +25143,7 @@ var localConfigProvider = {
23820
25143
  "claude",
23821
25144
  "codex"
23822
25145
  ]) {
23823
- import_fs8.default.mkdirSync(import_path15.default.join(dir, sub), { recursive: true });
25146
+ import_fs8.default.mkdirSync(import_path16.default.join(dir, sub), { recursive: true });
23824
25147
  }
23825
25148
  return dir;
23826
25149
  }
@@ -23842,12 +25165,18 @@ function toRtcIceServers(servers) {
23842
25165
  continue;
23843
25166
  }
23844
25167
  const [, scheme, hostname, port, transport] = turn;
25168
+ if (scheme === "turns" || transport === "tcp") continue;
23845
25169
  out.push({
23846
25170
  hostname,
23847
25171
  port: Number(port ?? (scheme === "turns" ? 5349 : 3478)),
23848
25172
  username: server.username,
23849
25173
  password: server.credential,
23850
- relayType: scheme === "turns" ? import_node_datachannel.RelayType.TurnTls : transport === "tcp" ? import_node_datachannel.RelayType.TurnTcp : import_node_datachannel.RelayType.TurnUdp
25174
+ // String literals, not the RelayType members: it is a `const enum`,
25175
+ // so it is erased at compile time and has no runtime object to read —
25176
+ // `RelayType.TurnUdp` threw, which killed the offer handler before it
25177
+ // could answer and made every connection fail, not just the relayed
25178
+ // ones. The enum's values are exactly these strings.
25179
+ relayType: scheme === "turns" ? "TurnTls" : transport === "tcp" ? "TurnTcp" : "TurnUdp"
23851
25180
  });
23852
25181
  }
23853
25182
  }
@@ -23988,15 +25317,15 @@ async function startDaemon(config) {
23988
25317
  }
23989
25318
  );
23990
25319
  const indexCandidates = [
23991
- import_path16.default.join(__dirname, "..", "index"),
23992
- import_path16.default.join(__dirname, "..", "..", "index", "image")
25320
+ import_path17.default.join(__dirname, "..", "index"),
25321
+ import_path17.default.join(__dirname, "..", "..", "index", "image")
23993
25322
  ];
23994
25323
  const indexDir = indexCandidates.find((p) => import_fs9.default.existsSync(p)) ?? indexCandidates[0];
23995
25324
  const odIndex = new IndexService({
23996
25325
  controlToken: (0, import_crypto7.createHash)("sha256").update(config.authToken).digest("hex"),
23997
25326
  embedUrl: () => config.serverUrl,
23998
25327
  indexDir,
23999
- dataDir: import_path16.default.join(CONFIG_DIR, "index-data")
25328
+ dataDir: import_path17.default.join(CONFIG_DIR, "index-data")
24000
25329
  });
24001
25330
  void odIndex.ensureRunning().catch((err) => {
24002
25331
  console.error(
@@ -24133,16 +25462,50 @@ async function startDaemon(config) {
24133
25462
  console.log(`Received WebRTC offer from ${data.fromSocketId}`);
24134
25463
  const scope = toChannelScope(data.scope);
24135
25464
  try {
24136
- const iceServers = toRtcIceServers(data.iceServers);
25465
+ let iceServers;
25466
+ try {
25467
+ iceServers = toRtcIceServers(data.iceServers);
25468
+ } catch (error) {
25469
+ console.error("[webrtc] ICE server list unusable, using STUN:", error);
25470
+ iceServers = ["stun:stun.l.google.com:19302"];
25471
+ }
24137
25472
  console.log(
24138
25473
  `[webrtc] ICE servers: ${iceServers.map(
24139
25474
  (u) => typeof u === "string" ? u : `${u.relayType}:${u.hostname}:${u.port} user=${u.username?.slice(0, 8)}\u2026`
24140
25475
  ).join(", ")}`
24141
25476
  );
24142
25477
  const peer = new import_node_datachannel.PeerConnection("runner", { iceServers });
25478
+ const view = {
25479
+ local: /* @__PURE__ */ new Set(),
25480
+ remote: /* @__PURE__ */ new Set(),
25481
+ relays: 0,
25482
+ gathering: "new",
25483
+ ice: "new"
25484
+ };
25485
+ const candidateType = (candidate) => /\btyp (\w+)/.exec(candidate)?.[1] ?? "unknown";
25486
+ let reported = false;
25487
+ const report = (outcome) => {
25488
+ if (reported || !data.linkId) return;
25489
+ reported = true;
25490
+ socket.emit("runnerLinkReport", {
25491
+ linkId: data.linkId,
25492
+ peerSocketId: data.fromSocketId,
25493
+ outcome,
25494
+ detail: [
25495
+ `ice=${view.ice} gathering=${view.gathering} relays=${view.relays}`,
25496
+ `local=${[...view.local].join(",") || "none"}`,
25497
+ `remote=${[...view.remote].join(",") || "none"}`,
25498
+ // Hostname and port only — the credential never leaves the runner.
25499
+ `turn=${iceServers.map(
25500
+ (u) => typeof u === "string" ? u : `${u.relayType}:${u.hostname}:${u.port}`
25501
+ ).join(" ")}`
25502
+ ].join("\n")
25503
+ });
25504
+ };
24143
25505
  const iceCandidateHandler = (msg) => {
24144
25506
  if (msg.fromSocketId === data.fromSocketId) {
24145
25507
  console.log(`[webrtc] remote ICE candidate: ${msg.candidate.candidate.slice(0, 60)}...`);
25508
+ view.remote.add(candidateType(msg.candidate.candidate));
24146
25509
  peer.addRemoteCandidate(
24147
25510
  msg.candidate.candidate,
24148
25511
  msg.candidate.sdpMid || ""
@@ -24158,18 +25521,31 @@ async function startDaemon(config) {
24158
25521
  };
24159
25522
  peer.onStateChange((state) => {
24160
25523
  console.log(`[webrtc] peer state: ${state}`);
25524
+ view.ice = state;
25525
+ if (state === "connected") report("connected");
24161
25526
  if (state === "failed" || state === "closed" || state === "disconnected") {
25527
+ if (state === "failed") report("failed");
24162
25528
  detachIceHandler();
24163
25529
  }
24164
25530
  });
24165
25531
  peer.onGatheringStateChange((state) => {
24166
25532
  console.log(`[webrtc] gathering state: ${state}`);
25533
+ view.gathering = state;
24167
25534
  });
24168
25535
  peer.onLocalCandidate((candidate, mid) => {
24169
25536
  console.log(`[webrtc] local ICE candidate: ${candidate.slice(0, 60)}...`);
25537
+ const type = candidateType(candidate);
25538
+ view.local.add(type);
25539
+ if (type === "relay") view.relays += 1;
24170
25540
  socket.emit("runnerIceCandidate", {
24171
25541
  targetSocketId: data.fromSocketId,
24172
- candidate: { candidate, sdpMid: mid, sdpMLineIndex: null }
25542
+ // 0 rather than null. A browser accepts a null index, but a mobile
25543
+ // client on the New Architecture passes this straight into codegen'd
25544
+ // JSI bindings that type-check it as a number, and null throws there —
25545
+ // rejecting every candidate we send and stranding the link at
25546
+ // checking. sdpMid identifies the m-line on its own, so the index is
25547
+ // redundant here anyway.
25548
+ candidate: { candidate, sdpMid: mid, sdpMLineIndex: 0 }
24173
25549
  });
24174
25550
  });
24175
25551
  peer.onLocalDescription((sdp, _type) => {