@tablation/crew 0.99.1 → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +803 -296
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -45,12 +45,12 @@ function periscopeSummary(cfg) {
45
45
  if (routes.length === 0) return `off (no enabled route sets hostPeriscopes: true; ${limits})`;
46
46
  return `ON for ${routes.join(", ")} (host ${cfg.ship.previewHost ?? "NOT SET (ship.previewHost)"}, ${limits})`;
47
47
  }
48
- async function previewHostProblem(cfg, resolve6 = (h) => lookup(h)) {
48
+ async function previewHostProblem(cfg, resolve7 = (h) => lookup(h)) {
49
49
  if (periscopeRoutes(cfg).length === 0) return void 0;
50
50
  const host = cfg.ship.previewHost;
51
51
  if (!host) return "ship.previewHost is not set \u2014 a route hosts periscopes, so Passengers have no host to be sent to";
52
52
  try {
53
- await resolve6(host);
53
+ await resolve7(host);
54
54
  } catch {
55
55
  return `ship.previewHost "${host}" does not resolve from this machine`;
56
56
  }
@@ -140,11 +140,11 @@ function urlPort(url) {
140
140
  }
141
141
  }
142
142
  function portListening(port, timeoutMs = 500) {
143
- return new Promise((resolve6) => {
143
+ return new Promise((resolve7) => {
144
144
  const s = connect({ port, host: "127.0.0.1" });
145
145
  const done = (ok) => {
146
146
  s.destroy();
147
- resolve6(ok);
147
+ resolve7(ok);
148
148
  };
149
149
  s.setTimeout(timeoutMs, () => done(false));
150
150
  s.once("connect", () => done(true));
@@ -583,8 +583,8 @@ function removeClaudeMcp(o) {
583
583
  if (got.missing) return `mcp: \`${o.bin}\` not found, so Claude Code was not touched`;
584
584
  if (got.status !== 0) return `mcp: no ${MCP_SERVER_NAME} server registered with Claude Code`;
585
585
  if (o.dryRun) return `(dry run) would remove the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)`;
586
- const rm = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
587
- return rm.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm.stderr.trim() || `exit ${rm.status}`})`;
586
+ const rm2 = runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
587
+ return rm2.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm2.stderr.trim() || `exit ${rm2.status}`})`;
588
588
  }
589
589
 
590
590
  // src/launcher.ts
@@ -620,10 +620,10 @@ function selectsCustomModelProvider(args) {
620
620
  }
621
621
  return false;
622
622
  }
623
- function parseOmlxSettings(text) {
623
+ function parseOmlxSettings(text2) {
624
624
  let raw;
625
625
  try {
626
- raw = JSON.parse(text);
626
+ raw = JSON.parse(text2);
627
627
  } catch {
628
628
  return void 0;
629
629
  }
@@ -844,32 +844,32 @@ var BLOCK_RE = new RegExp(`${escapeRe(PAIR_BLOCK_START)}[\\s\\S]*?${escapeRe(PAI
844
844
  function escapeRe(s) {
845
845
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
846
846
  }
847
- function hasPairBlock(text) {
848
- return BLOCK_RE.test(text);
847
+ function hasPairBlock(text2) {
848
+ return BLOCK_RE.test(text2);
849
849
  }
850
- function pairBlockIsBroken(text) {
851
- return !hasPairBlock(text) && (text.includes(PAIR_BLOCK_START) || text.includes(PAIR_BLOCK_END));
850
+ function pairBlockIsBroken(text2) {
851
+ return !hasPairBlock(text2) && (text2.includes(PAIR_BLOCK_START) || text2.includes(PAIR_BLOCK_END));
852
852
  }
853
- function upsertPairBlock(text, block) {
854
- if (hasPairBlock(text)) return text.replace(BLOCK_RE, () => `${block}
853
+ function upsertPairBlock(text2, block) {
854
+ if (hasPairBlock(text2)) return text2.replace(BLOCK_RE, () => `${block}
855
855
  `);
856
- if (text.trim() === "") return `${block}
856
+ if (text2.trim() === "") return `${block}
857
857
  `;
858
- return `${text.replace(/\n*$/, "")}
858
+ return `${text2.replace(/\n*$/, "")}
859
859
 
860
860
  ${block}
861
861
  `;
862
862
  }
863
- function removePairBlock(text) {
864
- if (!hasPairBlock(text)) return text;
865
- return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
863
+ function removePairBlock(text2) {
864
+ if (!hasPairBlock(text2)) return text2;
865
+ return text2.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
866
866
  }
867
867
  function readIfExists(path) {
868
868
  return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
869
869
  }
870
- function writeFileMkdir(path, text) {
870
+ function writeFileMkdir(path, text2) {
871
871
  mkdirSync4(dirname2(path), { recursive: true });
872
- writeFileSync3(path, text);
872
+ writeFileSync3(path, text2);
873
873
  }
874
874
  function installBlockFile(path, harness, dryRun) {
875
875
  const before = readIfExists(path) ?? "";
@@ -889,9 +889,9 @@ function removeBlockFile(path, dryRun) {
889
889
  return true;
890
890
  }
891
891
  function blockVerdict(path, harness) {
892
- const text = readIfExists(path);
893
- if (text === void 0 || !hasPairBlock(text)) return { ok: false, detail: `no managed block in ${path}` };
894
- if (!text.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
892
+ const text2 = readIfExists(path);
893
+ if (text2 === void 0 || !hasPairBlock(text2)) return { ok: false, detail: `no managed block in ${path}` };
894
+ if (!text2.includes(pairBlock(harness))) return { ok: false, detail: `the managed block in ${path} is out of date \u2014 run crew connect` };
895
895
  return { ok: true, detail: "" };
896
896
  }
897
897
  var HEADER_RE = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
@@ -913,24 +913,24 @@ function tomlTableRange(lines, name) {
913
913
  return [start, end];
914
914
  }
915
915
  var tomlString = (v) => JSON.stringify(v);
916
- function upsertTomlTable(text, name, table2) {
917
- const lines = text.split("\n");
916
+ function upsertTomlTable(text2, name, table2) {
917
+ const lines = text2.split("\n");
918
918
  const r = tomlTableRange(lines, name);
919
919
  if (r) {
920
920
  lines.splice(r[0], r[1] - r[0], ...table2);
921
921
  return lines.join("\n");
922
922
  }
923
- const body = text.replace(/\n*$/, "");
923
+ const body = text2.replace(/\n*$/, "");
924
924
  return body === "" ? `${table2.join("\n")}
925
925
  ` : `${body}
926
926
 
927
927
  ${table2.join("\n")}
928
928
  `;
929
929
  }
930
- function removeTomlTable(text, name) {
931
- const lines = text.split("\n");
930
+ function removeTomlTable(text2, name) {
931
+ const lines = text2.split("\n");
932
932
  let r = tomlTableRange(lines, name);
933
- if (!r) return text;
933
+ if (!r) return text2;
934
934
  while (r) {
935
935
  let [start, end] = r;
936
936
  if (start > 0 && lines[start - 1].trim() === "") start--;
@@ -1164,10 +1164,10 @@ var geminiPaths = (home) => ({
1164
1164
  config: join8(home, ".gemini", "settings.json")
1165
1165
  });
1166
1166
  function readGeminiSettings(path) {
1167
- const text = readIfExists(path);
1168
- if (text === void 0 || text.trim() === "") return {};
1167
+ const text2 = readIfExists(path);
1168
+ if (text2 === void 0 || text2.trim() === "") return {};
1169
1169
  try {
1170
- const j = JSON.parse(text);
1170
+ const j = JSON.parse(text2);
1171
1171
  return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
1172
1172
  } catch {
1173
1173
  return void 0;
@@ -1267,10 +1267,10 @@ function vscodeUserDirs(o) {
1267
1267
  }
1268
1268
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
1269
1269
  function readVscodeMcp(path) {
1270
- const text = readIfExists(path);
1271
- if (text === void 0 || text.trim() === "") return {};
1270
+ const text2 = readIfExists(path);
1271
+ if (text2 === void 0 || text2.trim() === "") return {};
1272
1272
  try {
1273
- const j = JSON.parse(text);
1273
+ const j = JSON.parse(text2);
1274
1274
  return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
1275
1275
  } catch {
1276
1276
  return void 0;
@@ -1446,9 +1446,9 @@ function mapGeminiLine(line) {
1446
1446
  function createGeminiStreamMapper() {
1447
1447
  let pending = "";
1448
1448
  const flush = () => {
1449
- const text = pending;
1449
+ const text2 = pending;
1450
1450
  pending = "";
1451
- return text ? [{ kind: "text", text }] : [];
1451
+ return text2 ? [{ kind: "text", text: text2 }] : [];
1452
1452
  };
1453
1453
  return {
1454
1454
  flush,
@@ -1758,9 +1758,9 @@ function storeForgeToken(forges, host, token, stateDir, env = process.env) {
1758
1758
  if (!token.trim()) throw new ForgeError("empty token");
1759
1759
  i.write(f.entry.token, token.trim(), { host: f.host, stateDir, env });
1760
1760
  }
1761
- function parseCredentialInput(text) {
1761
+ function parseCredentialInput(text2) {
1762
1762
  const out = {};
1763
- for (const line of text.split("\n")) {
1763
+ for (const line of text2.split("\n")) {
1764
1764
  if (line === "") break;
1765
1765
  const eq2 = line.indexOf("=");
1766
1766
  if (eq2 > 0) out[line.slice(0, eq2)] = line.slice(eq2 + 1);
@@ -2148,10 +2148,10 @@ function rejectUnknown(obj, allowed, where, file) {
2148
2148
  );
2149
2149
  }
2150
2150
  }
2151
- function parseRepoConfig(text, file) {
2151
+ function parseRepoConfig(text2, file) {
2152
2152
  let raw;
2153
2153
  try {
2154
- raw = file.endsWith(".json") ? JSON.parse(text) : parse(text);
2154
+ raw = file.endsWith(".json") ? JSON.parse(text2) : parse(text2);
2155
2155
  } catch (e) {
2156
2156
  throw new RepoConfigError(`${file}: could not be parsed \u2014 ${e.message}`);
2157
2157
  }
@@ -2667,9 +2667,9 @@ function parseShipEnv(v) {
2667
2667
  if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") {
2668
2668
  throw new ConfigError(`ship.env.${k} must be a string, number or boolean`);
2669
2669
  }
2670
- const text = String(val);
2671
- if (/[\r\n\0]/.test(text)) throw new ConfigError(`ship.env.${k} must be a single line`);
2672
- out[k] = text;
2670
+ const text2 = String(val);
2671
+ if (/[\r\n\0]/.test(text2)) throw new ConfigError(`ship.env.${k} must be a single line`);
2672
+ out[k] = text2;
2673
2673
  }
2674
2674
  return Object.keys(out).length ? out : void 0;
2675
2675
  }
@@ -3639,10 +3639,10 @@ function runScript(script, opts) {
3639
3639
  let output = "";
3640
3640
  let pending = "";
3641
3641
  const absorb = (buf) => {
3642
- const text = buf.toString();
3643
- output += text;
3642
+ const text2 = buf.toString();
3643
+ output += text2;
3644
3644
  if (!opts.onLine) return;
3645
- pending += text;
3645
+ pending += text2;
3646
3646
  const lines = pending.split("\n");
3647
3647
  pending = lines.pop() ?? "";
3648
3648
  for (const l of lines) opts.onLine(l);
@@ -4062,9 +4062,9 @@ function planCron(ship, crewHome, job) {
4062
4062
  function unitAbandonsAgents(plan, read) {
4063
4063
  const path = plan.unitPaths[0];
4064
4064
  if (!path || plan.mechanism !== "launchd" && plan.mechanism !== "systemd") return null;
4065
- const text = read(path);
4066
- if (text === null) return null;
4067
- return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text) : /^KillMode=process\s*$/m.test(text);
4065
+ const text2 = read(path);
4066
+ if (text2 === null) return null;
4067
+ return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text2) : /^KillMode=process\s*$/m.test(text2);
4068
4068
  }
4069
4069
  function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
4070
4070
  if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
@@ -4249,8 +4249,8 @@ function launchAgentsDir() {
4249
4249
  function xmlUnescape(s) {
4250
4250
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4251
4251
  }
4252
- function programFromPlist(text) {
4253
- const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text);
4252
+ function programFromPlist(text2) {
4253
+ const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text2);
4254
4254
  if (!block) return null;
4255
4255
  const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
4256
4256
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
@@ -4316,9 +4316,9 @@ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
4316
4316
  function removeAllLaunchdUnits(dryRun, log, dir, loaded) {
4317
4317
  for (const unit of listLaunchdUnits(dir, loaded)) removeLaunchdUnit(unit, dryRun, log);
4318
4318
  }
4319
- function parseLaunchctlList(text) {
4319
+ function parseLaunchctlList(text2) {
4320
4320
  const jobs = /* @__PURE__ */ new Map();
4321
- for (const line of text.split("\n")) {
4321
+ for (const line of text2.split("\n")) {
4322
4322
  const cols = line.split(" ");
4323
4323
  if (cols.length !== 3 || cols[2] === "Label") continue;
4324
4324
  jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
@@ -4342,8 +4342,8 @@ function readCrontab() {
4342
4342
  const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
4343
4343
  return res.status === 0 ? res.stdout ?? "" : "";
4344
4344
  }
4345
- function writeCrontab(text) {
4346
- spawnSync4("crontab", ["-"], { input: text, encoding: "utf8" });
4345
+ function writeCrontab(text2) {
4346
+ spawnSync4("crontab", ["-"], { input: text2, encoding: "utf8" });
4347
4347
  }
4348
4348
  function addCrontabLine(marker, line) {
4349
4349
  const existing = readCrontab();
@@ -5356,10 +5356,10 @@ function formatPushAuth(repo, r) {
5356
5356
  return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
5357
5357
  `;
5358
5358
  }
5359
- async function settleRepos(items, nameOf, resolve6) {
5359
+ async function settleRepos(items, nameOf, resolve7) {
5360
5360
  const out = await Promise.all(items.map(async (t) => {
5361
5361
  try {
5362
- return { ok: true, value: await resolve6(t) };
5362
+ return { ok: true, value: await resolve7(t) };
5363
5363
  } catch (e) {
5364
5364
  return { ok: false, repo: nameOf(t), detail: e.message };
5365
5365
  }
@@ -6660,8 +6660,8 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
6660
6660
 
6661
6661
  // src/ticket-attribution.ts
6662
6662
  var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
6663
- function ticketMentions(text) {
6664
- return [...text.matchAll(TICKET_KEY_RE)].map((m) => m[0]);
6663
+ function ticketMentions(text2) {
6664
+ return [...text2.matchAll(TICKET_KEY_RE)].map((m) => m[0]);
6665
6665
  }
6666
6666
  var TicketAttributionTracker = class {
6667
6667
  #tokensByTicket = /* @__PURE__ */ new Map();
@@ -6680,13 +6680,13 @@ var TicketAttributionTracker = class {
6680
6680
  return this.#current;
6681
6681
  }
6682
6682
  /** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
6683
- note(text) {
6684
- const mentions = ticketMentions(text);
6683
+ note(text2) {
6684
+ const mentions = ticketMentions(text2);
6685
6685
  if (mentions.length > 0) this.#current = mentions[0];
6686
6686
  }
6687
6687
  /** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
6688
- add(text, tokens) {
6689
- const mentions = ticketMentions(text);
6688
+ add(text2, tokens) {
6689
+ const mentions = ticketMentions(text2);
6690
6690
  if (mentions.length > 0) this.#current = mentions[0];
6691
6691
  if (this.#current === void 0 || tokens <= 0) return;
6692
6692
  this.#turns++;
@@ -6783,10 +6783,10 @@ function resultText(content) {
6783
6783
  if (Array.isArray(content)) return content.map((c) => c && typeof c.text === "string" ? c.text : "").join("\n");
6784
6784
  return "";
6785
6785
  }
6786
- function isTransportFailure(isError, text) {
6786
+ function isTransportFailure(isError, text2) {
6787
6787
  if (!isError) {
6788
6788
  try {
6789
- const doc = JSON.parse(text);
6789
+ const doc = JSON.parse(text2);
6790
6790
  if (doc && typeof doc === "object") {
6791
6791
  const code = doc.statusCode;
6792
6792
  return typeof code === "number" && code >= 500;
@@ -6794,7 +6794,7 @@ function isTransportFailure(isError, text) {
6794
6794
  } catch {
6795
6795
  }
6796
6796
  }
6797
- return TRANSPORT_FAILURE.test(text);
6797
+ return TRANSPORT_FAILURE.test(text2);
6798
6798
  }
6799
6799
  var HandoffTranscriptTracker = class {
6800
6800
  open = /* @__PURE__ */ new Map();
@@ -8653,15 +8653,15 @@ async function replayPendingHandoffs(i) {
8653
8653
  continue;
8654
8654
  }
8655
8655
  if (v.kind === "unverified") throw new Error(v.why);
8656
- const text = rec.comment ?? (rec.commentLanded ? "" : rec.finalText);
8657
- if (text) {
8658
- const head = firstLine(text);
8656
+ const text2 = rec.comment ?? (rec.commentLanded ? "" : rec.finalText);
8657
+ if (text2) {
8658
+ const head = firstLine(text2);
8659
8659
  const cc = i.contract.comments;
8660
8660
  const seen = (await i.writer.comments()).some((c) => {
8661
8661
  const row = c;
8662
8662
  return row[cc.parent] === t.id && row[cc.author] === seatId && firstLine(String(row[cc.body] ?? "")) === head;
8663
8663
  });
8664
- if (!seen) await i.writer.postComment(t.id, text, seatId);
8664
+ if (!seen) await i.writer.postComment(t.id, text2, seatId);
8665
8665
  }
8666
8666
  await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), seatId, "pushed");
8667
8667
  await i.writer.updateTicket(t.id, rec.patch);
@@ -8690,15 +8690,15 @@ var SkillIncludeError = class extends Error {
8690
8690
  var MAX_INCLUDE_DEPTH = 3;
8691
8691
  var MAX_INCLUDED_BYTES = 2e5;
8692
8692
  var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
8693
- function skillMarkerNames(text) {
8693
+ function skillMarkerNames(text2) {
8694
8694
  const names = [];
8695
- for (const m of text.matchAll(MARKER)) {
8695
+ for (const m of text2.matchAll(MARKER)) {
8696
8696
  const name = m[1].trim();
8697
8697
  if (name && !names.includes(name)) names.push(name);
8698
8698
  }
8699
8699
  return names;
8700
8700
  }
8701
- function expandSkillMarkers(text, lookup2, opts = {}) {
8701
+ function expandSkillMarkers(text2, lookup2, opts = {}) {
8702
8702
  const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
8703
8703
  const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
8704
8704
  const warnings = [];
@@ -8725,7 +8725,7 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
8725
8725
  included += bytes;
8726
8726
  return expand2(trimmed, [...chain, name]);
8727
8727
  });
8728
- return { text: expand2(text, []), warnings };
8728
+ return { text: expand2(text2, []), warnings };
8729
8729
  }
8730
8730
 
8731
8731
  // src/skills.ts
@@ -8736,9 +8736,9 @@ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError
8736
8736
 
8737
8737
  // src/prompt-additions.ts
8738
8738
  var PROMPT_ADDITIONS_SEPARATOR = "---\n\nAdditional instructions from this workspace. Where they conflict with anything above, these take precedence.";
8739
- function appendAdditions(text, additions) {
8740
- if (typeof additions !== "string" || additions.trim() === "") return text;
8741
- return `${text}
8739
+ function appendAdditions(text2, additions) {
8740
+ if (typeof additions !== "string" || additions.trim() === "") return text2;
8741
+ return `${text2}
8742
8742
 
8743
8743
  ${PROMPT_ADDITIONS_SEPARATOR}
8744
8744
 
@@ -9213,8 +9213,8 @@ function describeCrewLink(o) {
9213
9213
  var SkillSyncError = class extends Error {
9214
9214
  };
9215
9215
  function parseSkillFile(path) {
9216
- const text = readFileSync18(path, "utf8");
9217
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
9216
+ const text2 = readFileSync18(path, "utf8");
9217
+ const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
9218
9218
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
9219
9219
  const frontmatter = match[1];
9220
9220
  const body = match[2];
@@ -9403,8 +9403,8 @@ function isEditedSkillRow(row, localPrompt, cached) {
9403
9403
  function skillMarkerWarnings(promptsDir, personaPrompts) {
9404
9404
  const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
9405
9405
  const out = [];
9406
- for (const [role, text] of Object.entries(personaPrompts)) {
9407
- for (const name of skillMarkerNames(text ?? "")) {
9406
+ for (const [role, text2] of Object.entries(personaPrompts)) {
9407
+ for (const name of skillMarkerNames(text2 ?? "")) {
9408
9408
  if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
9409
9409
  }
9410
9410
  }
@@ -9741,15 +9741,15 @@ var FileTail = class {
9741
9741
  return "";
9742
9742
  }
9743
9743
  try {
9744
- let text = "";
9744
+ let text2 = "";
9745
9745
  const chunk = Buffer.alloc(64 * 1024);
9746
9746
  for (; ; ) {
9747
9747
  const n = readSync(fd, chunk, 0, chunk.length, this.offset);
9748
9748
  if (n <= 0) break;
9749
9749
  this.offset += n;
9750
- text += this.decoder.write(chunk.subarray(0, n));
9750
+ text2 += this.decoder.write(chunk.subarray(0, n));
9751
9751
  }
9752
- return text;
9752
+ return text2;
9753
9753
  } finally {
9754
9754
  closeSync3(fd);
9755
9755
  }
@@ -9788,8 +9788,8 @@ var RunObserver = class {
9788
9788
  this.liveTicket = id.ticket ?? void 0;
9789
9789
  this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
9790
9790
  }
9791
- feed(text) {
9792
- this.buf += text;
9791
+ feed(text2) {
9792
+ this.buf += text2;
9793
9793
  const lines = this.buf.split("\n");
9794
9794
  this.buf = lines.pop() ?? "";
9795
9795
  for (const line of lines) this.handleLine(line);
@@ -10054,7 +10054,7 @@ function spawnAgent(plan, emit, hintMoved) {
10054
10054
  for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
10055
10055
  const eventsSink = openSink(plan.eventsPath);
10056
10056
  const id = identityOf(plan, adapter.harness);
10057
- return new Promise((resolve6, reject) => {
10057
+ return new Promise((resolve7, reject) => {
10058
10058
  let streamFd;
10059
10059
  try {
10060
10060
  mkdirSync12(dirname8(plan.streamPath), { recursive: true });
@@ -10138,7 +10138,7 @@ function spawnAgent(plan, emit, hintMoved) {
10138
10138
  plan.agentLog?.apiKey,
10139
10139
  hintMoved
10140
10140
  );
10141
- resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
10141
+ resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
10142
10142
  });
10143
10143
  child.stdin?.on("error", (err3) => {
10144
10144
  emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
@@ -10471,7 +10471,7 @@ var SELFTEST_TIMEOUT_MS = 12e4;
10471
10471
  var KEY_VAR = "CREW_API_KEY";
10472
10472
  var SELFTEST_PROMPT = "This is a read-only connectivity check. Use the tracker MCP tool to read this workspace (the workspaces find-one tool, using the workspace id below) and reply with the workspace's name alone, on one line. Do not write anything, do not create files, and do not use any other tool.\n\nWorkspace id: ";
10473
10473
  var realDeps = {
10474
- run: (o) => new Promise((resolve6) => {
10474
+ run: (o) => new Promise((resolve7) => {
10475
10475
  let stdout = "";
10476
10476
  let stderr = "";
10477
10477
  let timedOut = false;
@@ -10479,7 +10479,7 @@ var realDeps = {
10479
10479
  const done = (r) => {
10480
10480
  if (!settled) {
10481
10481
  settled = true;
10482
- resolve6(r);
10482
+ resolve7(r);
10483
10483
  }
10484
10484
  };
10485
10485
  const child = spawn3(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
@@ -10588,12 +10588,12 @@ function classify(spec, run2, seen) {
10588
10588
  return fail("binary-not-found", `could not start \`${bin}\` (${run2.spawnError.message})`);
10589
10589
  }
10590
10590
  if (run2.timedOut) return fail("timeout", "no answer within the time limit");
10591
- const text = `${seen.toolOutput}
10591
+ const text2 = `${seen.toolOutput}
10592
10592
  ${run2.stderr}`;
10593
10593
  if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run2.stderr)) {
10594
10594
  return fail("key-refused", "the tracker returned 401/403 to the MCP call");
10595
10595
  }
10596
- if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text}
10596
+ if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
10597
10597
  ${seen.texts.join("\n")}`)) {
10598
10598
  return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
10599
10599
  }
@@ -10685,7 +10685,7 @@ function doctorSelftestLines(stored, toolchains) {
10685
10685
  }
10686
10686
 
10687
10687
  // src/cli.ts
10688
- import { resolve as resolve5, dirname as dirname22, join as join44, basename as basename13 } from "path";
10688
+ import { resolve as resolve6, dirname as dirname23, join as join46, basename as basename14 } from "path";
10689
10689
  import { fileURLToPath } from "url";
10690
10690
  import { hostname as hostname3, homedir as homedir10 } from "os";
10691
10691
  import { spawnSync as spawnSync8 } from "child_process";
@@ -12255,13 +12255,13 @@ function writeDigest(o, d, role, stateDir, runId, own) {
12255
12255
  blockerInfo: d.info
12256
12256
  };
12257
12257
  const body = role === "qa" ? qaDigest(input) : buildingDigest(input);
12258
- const text = own ? `${ownRunBanner(own)}
12258
+ const text2 = own ? `${ownRunBanner(own)}
12259
12259
 
12260
12260
  ${body}` : body;
12261
12261
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
12262
- writeFileSync17(path, text);
12263
- o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
12264
- data: { role, bytes: Buffer.byteLength(text) }
12262
+ writeFileSync17(path, text2);
12263
+ o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
12264
+ data: { role, bytes: Buffer.byteLength(text2) }
12265
12265
  });
12266
12266
  return true;
12267
12267
  } catch (e) {
@@ -12825,10 +12825,10 @@ function renderFullConfig(a, routeBlock) {
12825
12825
  routes:
12826
12826
  ${routeBlock}`;
12827
12827
  }
12828
- function writeNewConfig(path, text) {
12828
+ function writeNewConfig(path, text2) {
12829
12829
  mkdirSync16(dirname11(path), { recursive: true });
12830
12830
  try {
12831
- writeFileSync18(path, text, { mode: 384, flag: "wx" });
12831
+ writeFileSync18(path, text2, { mode: 384, flag: "wx" });
12832
12832
  return true;
12833
12833
  } catch (e) {
12834
12834
  if (e.code === "EEXIST") return false;
@@ -12865,11 +12865,11 @@ import { homedir as homedir8 } from "os";
12865
12865
  import { dirname as dirname12, join as join28 } from "path";
12866
12866
  import { delimiter as delimiter2 } from "path";
12867
12867
  var CHOICE_EXIT_CODE = 3;
12868
- function createEmitter(json, write) {
12868
+ function createEmitter(json2, write) {
12869
12869
  return {
12870
- json,
12870
+ json: json2,
12871
12871
  emit(event, message, data = {}) {
12872
- if (!json) return;
12872
+ if (!json2) return;
12873
12873
  write(`${JSON.stringify({ ...data, event, message })}
12874
12874
  `);
12875
12875
  }
@@ -12976,8 +12976,8 @@ function matchRepoName(origin, resolved) {
12976
12976
  }
12977
12977
  return void 0;
12978
12978
  }
12979
- function routeNamesOf(text) {
12980
- const routes = parseDocument(text).get("routes", true);
12979
+ function routeNamesOf(text2) {
12980
+ const routes = parseDocument(text2).get("routes", true);
12981
12981
  if (!isSeq(routes)) return [];
12982
12982
  return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
12983
12983
  }
@@ -13011,8 +13011,8 @@ function routeMapOf(doc, route2) {
13011
13011
  const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
13012
13012
  throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
13013
13013
  }
13014
- function configuredRepos(text, route2, base) {
13015
- const doc = parseDocument(text);
13014
+ function configuredRepos(text2, route2, base) {
13015
+ const doc = parseDocument(text2);
13016
13016
  const m = routeMapOf(doc, route2);
13017
13017
  const repos = m.get("repos");
13018
13018
  if (isMap(repos)) {
@@ -13026,8 +13026,8 @@ function configuredRepos(text, route2, base) {
13026
13026
  const dir = m.get("dir");
13027
13027
  return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
13028
13028
  }
13029
- function cloneBaseOf(text, route2, base, isCheckout) {
13030
- const m = routeMapOf(parseDocument(text), route2);
13029
+ function cloneBaseOf(text2, route2, base, isCheckout) {
13030
+ const m = routeMapOf(parseDocument(text2), route2);
13031
13031
  const repos = m.get("repos", true);
13032
13032
  if (isMap(repos) && repos.items.length > 0) return null;
13033
13033
  const dir = m.get("dir");
@@ -13035,8 +13035,8 @@ function cloneBaseOf(text, route2, base, isCheckout) {
13035
13035
  const abs = expandDir(String(dir), base);
13036
13036
  return isCheckout(abs) ? null : abs;
13037
13037
  }
13038
- function routeSetsHooks(text, route2, name) {
13039
- const m = routeMapOf(parseDocument(text), route2);
13038
+ function routeSetsHooks(text2, route2, name) {
13039
+ const m = routeMapOf(parseDocument(text2), route2);
13040
13040
  const repos = m.get("repos", true);
13041
13041
  let target = m;
13042
13042
  if (isMap(repos)) target = repos.get(name, true);
@@ -13143,12 +13143,12 @@ function planRepoAdd(i) {
13143
13143
  }
13144
13144
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
13145
13145
  }
13146
- function planRepoRemove(text, base, route2, path) {
13147
- const doc = parseDocument(text);
13146
+ function planRepoRemove(text2, base, route2, path) {
13147
+ const doc = parseDocument(text2);
13148
13148
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
13149
13149
  const m = routeMapOf(doc, route2);
13150
13150
  const target = expandDir(path, base);
13151
- const existing = configuredRepos(text, route2, base);
13151
+ const existing = configuredRepos(text2, route2, base);
13152
13152
  const hit = existing.find((e) => e.dir === target);
13153
13153
  if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
13154
13154
  const repos = m.get("repos", true);
@@ -13273,8 +13273,8 @@ async function gatherHealth(c, check) {
13273
13273
  function routesInScope(routes, named) {
13274
13274
  return named ? routes.filter((r) => r.route === named) : routes;
13275
13275
  }
13276
- function planEnable(text, route2) {
13277
- const doc = parseDocument2(text);
13276
+ function planEnable(text2, route2) {
13277
+ const doc = parseDocument2(text2);
13278
13278
  routeMapOf(doc, route2).set("enabled", true);
13279
13279
  return String(doc);
13280
13280
  }
@@ -13468,13 +13468,13 @@ function realProvisionDeps(crewHome) {
13468
13468
  enableRoute(route2) {
13469
13469
  const file = configFile();
13470
13470
  if (!file) return false;
13471
- const text = readFileSync23(file, "utf8");
13472
- const doc = parseDocument3(text);
13471
+ const text2 = readFileSync23(file, "utf8");
13472
+ const doc = parseDocument3(text2);
13473
13473
  const routes = doc.get("routes");
13474
13474
  const entry = routes?.items?.find((r) => r.get("route") === route2);
13475
13475
  if (!entry) throw new Error(`route ${route2} is not in ${file}`);
13476
13476
  if (entry.get("enabled") === true) return false;
13477
- writeFileSync19(file, planEnable(text, route2));
13477
+ writeFileSync19(file, planEnable(text2, route2));
13478
13478
  return true;
13479
13479
  },
13480
13480
  installState() {
@@ -15658,18 +15658,18 @@ function readFrom(file, from) {
15658
15658
  const buf = Buffer.alloc(size - from);
15659
15659
  readSync2(fd, buf, 0, buf.length, from);
15660
15660
  closeSync4(fd);
15661
- const text = buf.toString("utf8");
15662
- const lastNewline = text.lastIndexOf("\n");
15661
+ const text2 = buf.toString("utf8");
15662
+ const lastNewline = text2.lastIndexOf("\n");
15663
15663
  if (lastNewline === -1) return { events: [], offset: from };
15664
15664
  const events = [];
15665
- for (const line of text.slice(0, lastNewline).split("\n")) {
15665
+ for (const line of text2.slice(0, lastNewline).split("\n")) {
15666
15666
  if (!line.trim()) continue;
15667
15667
  try {
15668
15668
  events.push(JSON.parse(line));
15669
15669
  } catch {
15670
15670
  }
15671
15671
  }
15672
- return { events, offset: from + Buffer.byteLength(text.slice(0, lastNewline + 1), "utf8") };
15672
+ return { events, offset: from + Buffer.byteLength(text2.slice(0, lastNewline + 1), "utf8") };
15673
15673
  }
15674
15674
  function startWatch(o) {
15675
15675
  const out = o.out ?? ((s) => process.stdout.write(s));
@@ -15964,12 +15964,12 @@ function planNameSync(local, row, lastSynced) {
15964
15964
  if (rowMoved) return { drift: "row", action: "adopt" };
15965
15965
  return { drift: "local", action: "push" };
15966
15966
  }
15967
- function shipNameOfYaml(text) {
15968
- const v = parseDocument5(text).getIn(["ship", "name"]);
15967
+ function shipNameOfYaml(text2) {
15968
+ const v = parseDocument5(text2).getIn(["ship", "name"]);
15969
15969
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
15970
15970
  }
15971
- function setShipNameInYaml(text, name) {
15972
- const doc = parseDocument5(text);
15971
+ function setShipNameInYaml(text2, name) {
15972
+ const doc = parseDocument5(text2);
15973
15973
  doc.setIn(["ship", "name"], name);
15974
15974
  return doc.toString();
15975
15975
  }
@@ -16058,8 +16058,8 @@ async function reconcileShipName(i) {
16058
16058
  }
16059
16059
  if (result.status !== "ok") return result;
16060
16060
  if (result.action === "adopt" || result.action === "row-wins") {
16061
- const text = readFileSync27(i.configFile, "utf8");
16062
- writeFileSync22(i.configFile, setShipNameInYaml(text, result.name));
16061
+ const text2 = readFileSync27(i.configFile, "utf8");
16062
+ writeFileSync22(i.configFile, setShipNameInYaml(text2, result.name));
16063
16063
  i.ship.name = result.name;
16064
16064
  if (result.action === "row-wins") {
16065
16065
  i.emit?.warn(
@@ -16960,11 +16960,11 @@ function packageManager(f) {
16960
16960
  return "npm";
16961
16961
  }
16962
16962
  function fromPackageJson(f) {
16963
- const text = f.read("package.json");
16964
- if (text === void 0) return void 0;
16963
+ const text2 = f.read("package.json");
16964
+ if (text2 === void 0) return void 0;
16965
16965
  let pkg;
16966
16966
  try {
16967
- pkg = JSON.parse(text);
16967
+ pkg = JSON.parse(text2);
16968
16968
  } catch {
16969
16969
  return void 0;
16970
16970
  }
@@ -16987,11 +16987,11 @@ function fromPyproject(f) {
16987
16987
  return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
16988
16988
  }
16989
16989
  function fromMakefile(f) {
16990
- const text = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
16991
- if (text === void 0) return void 0;
16990
+ const text2 = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
16991
+ if (text2 === void 0) return void 0;
16992
16992
  const out = {};
16993
16993
  for (const target of ["test", "build"]) {
16994
- if (new RegExp(`^${target}\\s*:`, "m").test(text)) out[target] = `make ${target}`;
16994
+ if (new RegExp(`^${target}\\s*:`, "m").test(text2)) out[target] = `make ${target}`;
16995
16995
  }
16996
16996
  return out;
16997
16997
  }
@@ -17084,16 +17084,16 @@ function parseHooksArgs(args, routes) {
17084
17084
  if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD] [--json]");
17085
17085
  return { route: route2, name: rest[0] };
17086
17086
  }
17087
- function repoDirOf(text, base, route2, name) {
17088
- const repos = configuredRepos(text, route2, base);
17087
+ function repoDirOf(text2, base, route2, name) {
17088
+ const repos = configuredRepos(text2, route2, base);
17089
17089
  const hit = repos.find((r) => r.name === name);
17090
17090
  if (!hit) {
17091
17091
  throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
17092
17092
  }
17093
17093
  return hit.dir;
17094
17094
  }
17095
- function planRouteHooks(text, route2, name, hooks, opts = {}) {
17096
- const doc = parseDocument6(text);
17095
+ function planRouteHooks(text2, route2, name, hooks, opts = {}) {
17096
+ const doc = parseDocument6(text2);
17097
17097
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
17098
17098
  const m = routeMapOf(doc, route2);
17099
17099
  const repos = m.get("repos", true);
@@ -17129,19 +17129,19 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
17129
17129
  }
17130
17130
  return { text: String(doc), written, kept, replaced };
17131
17131
  }
17132
- function clearRouteHooks(text, route2, name) {
17133
- const doc = parseDocument6(text);
17132
+ function clearRouteHooks(text2, route2, name) {
17133
+ const doc = parseDocument6(text2);
17134
17134
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
17135
17135
  const m = routeMapOf(doc, route2);
17136
17136
  const repos = m.get("repos", true);
17137
17137
  let target = m;
17138
17138
  if (isMap2(repos)) {
17139
17139
  const entry = repos.get(name, true);
17140
- if (!isMap2(entry)) return { text, removed: [] };
17140
+ if (!isMap2(entry)) return { text: text2, removed: [] };
17141
17141
  target = entry;
17142
17142
  }
17143
17143
  const current = target.get("hooks", true);
17144
- if (!isMap2(current)) return { text, removed: [] };
17144
+ if (!isMap2(current)) return { text: text2, removed: [] };
17145
17145
  const removed = current.items.map((i) => String(i.key.value ?? i.key));
17146
17146
  target.delete("hooks");
17147
17147
  return { text: String(doc), removed };
@@ -17165,9 +17165,9 @@ function starterRepoConfig(hooks, release) {
17165
17165
  if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
17166
17166
  lines.push("");
17167
17167
  } else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
17168
- const text = lines.join("\n");
17169
- parseRepoConfig(text, ".crew.yaml");
17170
- return text;
17168
+ const text2 = lines.join("\n");
17169
+ parseRepoConfig(text2, ".crew.yaml");
17170
+ return text2;
17171
17171
  }
17172
17172
  function existingRepoConfig(f) {
17173
17173
  return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
@@ -17226,7 +17226,7 @@ function agentInvocation(spec, model) {
17226
17226
  });
17227
17227
  }
17228
17228
  function parseAgentStream(out, dir) {
17229
- let text = "";
17229
+ let text2 = "";
17230
17230
  let isError = false;
17231
17231
  const files = /* @__PURE__ */ new Set();
17232
17232
  let base = dir;
@@ -17248,19 +17248,19 @@ function parseAgentStream(out, dir) {
17248
17248
  if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
17249
17249
  }
17250
17250
  } else if (ev?.type === "result") {
17251
- text = typeof ev.result === "string" ? ev.result : "";
17251
+ text2 = typeof ev.result === "string" ? ev.result : "";
17252
17252
  isError = ev.is_error === true;
17253
17253
  }
17254
17254
  }
17255
- return { text, isError, filesRead: [...files].sort() };
17255
+ return { text: text2, isError, filesRead: [...files].sort() };
17256
17256
  }
17257
- function parseAgentHooks(text) {
17258
- const start = text.indexOf("{");
17259
- const end = text.lastIndexOf("}");
17257
+ function parseAgentHooks(text2) {
17258
+ const start = text2.indexOf("{");
17259
+ const end = text2.lastIndexOf("}");
17260
17260
  if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
17261
17261
  let obj;
17262
17262
  try {
17263
- obj = JSON.parse(text.slice(start, end + 1));
17263
+ obj = JSON.parse(text2.slice(start, end + 1));
17264
17264
  } catch {
17265
17265
  throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
17266
17266
  }
@@ -17292,7 +17292,7 @@ async function runAgentSuggest(o) {
17292
17292
  const before = porcelain(o.dir);
17293
17293
  const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
17294
17294
  const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
17295
- const run2 = await new Promise((resolve6) => {
17295
+ const run2 = await new Promise((resolve7) => {
17296
17296
  let out = "";
17297
17297
  let err3 = "";
17298
17298
  const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
@@ -17301,7 +17301,7 @@ async function runAgentSuggest(o) {
17301
17301
  if (settled) return;
17302
17302
  settled = true;
17303
17303
  signal.removeEventListener("abort", onAbort);
17304
- resolve6(r);
17304
+ resolve7(r);
17305
17305
  };
17306
17306
  const onAbort = () => {
17307
17307
  try {
@@ -17745,7 +17745,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
17745
17745
  }
17746
17746
 
17747
17747
  // src/cli.ts
17748
- import { realpathSync as realpathSync3, readFileSync as readFileSync36, existsSync as existsSync41, mkdirSync as mkdirSync26, writeFileSync as writeFileSync28, rmSync as rmSync19, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync18 } from "fs";
17748
+ import { realpathSync as realpathSync3, readFileSync as readFileSync37, existsSync as existsSync42, mkdirSync as mkdirSync27, writeFileSync as writeFileSync29, rmSync as rmSync20, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync19 } from "fs";
17749
17749
  import { createInterface } from "readline/promises";
17750
17750
 
17751
17751
  // src/periscope-cmd.ts
@@ -19073,7 +19073,7 @@ import { randomBytes as randomBytes2 } from "crypto";
19073
19073
  var DEFAULT_FLOOR_MS = 5e3;
19074
19074
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
19075
19075
  var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
19076
- var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
19076
+ var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
19077
19077
  async function runDaemonLoop(o) {
19078
19078
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
19079
19079
  const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
@@ -19130,15 +19130,15 @@ async function runDaemonLoop(o) {
19130
19130
  }
19131
19131
  }
19132
19132
  function latestMtime(path) {
19133
- let stat;
19133
+ let stat2;
19134
19134
  try {
19135
- stat = statSync12(path);
19135
+ stat2 = statSync12(path);
19136
19136
  } catch {
19137
19137
  return 0;
19138
19138
  }
19139
- if (stat.isFile()) return stat.mtimeMs;
19140
- if (!stat.isDirectory()) return 0;
19141
- let latest = stat.mtimeMs;
19139
+ if (stat2.isFile()) return stat2.mtimeMs;
19140
+ if (!stat2.isDirectory()) return 0;
19141
+ let latest = stat2.mtimeMs;
19142
19142
  for (const entry of readdirSync16(path, { withFileTypes: true })) {
19143
19143
  latest = Math.max(latest, latestMtime(join42(path, entry.name)));
19144
19144
  }
@@ -19331,8 +19331,8 @@ async function runOnePassUnguarded(o) {
19331
19331
  started.push(role);
19332
19332
  let ready = () => {
19333
19333
  };
19334
- const settled = new Promise((resolve6) => {
19335
- ready = resolve6;
19334
+ const settled = new Promise((resolve7) => {
19335
+ ready = resolve7;
19336
19336
  });
19337
19337
  const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
19338
19338
  breaker.recordSuccess(role);
@@ -19784,14 +19784,18 @@ function parseLine(line) {
19784
19784
  return null;
19785
19785
  }
19786
19786
  }
19787
- async function execute(h, args, streaming) {
19788
- const finish = (r) => {
19787
+ async function execute(h, args, streaming, after) {
19788
+ const finishRaw = (r) => {
19789
19789
  if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
19790
19790
  const done = lastJsonLine(r.stdout);
19791
19791
  return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
19792
19792
  };
19793
+ const finish = async (r) => {
19794
+ const f = finishRaw(r);
19795
+ return f.status === 200 && after ? { status: 200, body: await after(f.body) } : f;
19796
+ };
19793
19797
  if (!streaming) {
19794
- const f = finish(await h.run(args, () => {
19798
+ const f = await finish(await h.run(args, () => {
19795
19799
  }));
19796
19800
  return reply(f.status, f.body);
19797
19801
  }
@@ -19803,7 +19807,7 @@ async function execute(h, args, streaming) {
19803
19807
  const ev = parseLine(line);
19804
19808
  if (ev) write(ev);
19805
19809
  });
19806
- const f = finish(r);
19810
+ const f = await finish(r);
19807
19811
  write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
19808
19812
  }
19809
19813
  };
@@ -19867,7 +19871,10 @@ async function handlePeriscopes(h, q) {
19867
19871
  `--${b.outcome}`,
19868
19872
  ...b.allowEmpty === true ? ["--allow-empty"] : [],
19869
19873
  "--json"
19870
- ], q.wantsStream);
19874
+ ], q.wantsStream, h.mcp ? async (done) => {
19875
+ await h.mcp.onDown(picked, done);
19876
+ return done;
19877
+ } : void 0);
19871
19878
  }
19872
19879
  const repo = plainText(b.repo, "repo", true);
19873
19880
  const topic = plainText(b.topic, "topic", true);
@@ -19895,12 +19902,13 @@ async function handlePeriscopes(h, q) {
19895
19902
  h.previewHost,
19896
19903
  ...requestedBy ? ["--requested-by", requestedBy] : [],
19897
19904
  ...b.isolate === true ? ["--isolate"] : [],
19905
+ ...h.mcp ? ["--serve-mcp"] : [],
19898
19906
  "--no-open",
19899
19907
  "--json"
19900
- ], q.wantsStream);
19908
+ ], q.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
19901
19909
  }
19902
19910
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
19903
- return new Promise((resolve6) => {
19911
+ return new Promise((resolve7) => {
19904
19912
  const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
19905
19913
  let stdout = "";
19906
19914
  let stderr = "";
@@ -19909,9 +19917,9 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
19909
19917
  stdout += d.toString("utf8");
19910
19918
  });
19911
19919
  child.stderr.on("data", (d) => {
19912
- const text = d.toString("utf8");
19913
- stderr += text;
19914
- pending += text;
19920
+ const text2 = d.toString("utf8");
19921
+ stderr += text2;
19922
+ pending += text2;
19915
19923
  let i;
19916
19924
  while ((i = pending.indexOf("\n")) >= 0) {
19917
19925
  const line = pending.slice(0, i).trim();
@@ -19922,12 +19930,12 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
19922
19930
  const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
19923
19931
  child.on("error", (e) => {
19924
19932
  clearTimeout(t);
19925
- resolve6({ code: 127, stdout, stderr: `${stderr}${e.message}
19933
+ resolve7({ code: 127, stdout, stderr: `${stderr}${e.message}
19926
19934
  ` });
19927
19935
  });
19928
19936
  child.on("close", (code) => {
19929
19937
  clearTimeout(t);
19930
- resolve6({ code: code ?? 1, stdout, stderr });
19938
+ resolve7({ code: code ?? 1, stdout, stderr });
19931
19939
  });
19932
19940
  });
19933
19941
  }
@@ -20048,16 +20056,16 @@ function lastLines2(path, n) {
20048
20056
  const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
20049
20057
  if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
20050
20058
  let start = st.size;
20051
- let text = "";
20052
- while (start > 0 && text.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
20059
+ let text2 = "";
20060
+ while (start > 0 && text2.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
20053
20061
  const step = Math.min(64 * 1024, start);
20054
20062
  start -= step;
20055
- text = readRange(path, start, step).text + text;
20063
+ text2 = readRange(path, start, step).text + text2;
20056
20064
  }
20057
- let lines = text.split("\n");
20065
+ let lines = text2.split("\n");
20058
20066
  if (start > 0) lines = lines.slice(1);
20059
20067
  let end = st.size;
20060
- if (!text.endsWith("\n")) {
20068
+ if (!text2.endsWith("\n")) {
20061
20069
  const frag = lines.pop() ?? "";
20062
20070
  end -= Buffer.byteLength(frag);
20063
20071
  } else lines.pop();
@@ -20099,9 +20107,9 @@ function readTail(live, opts) {
20099
20107
  off = 0;
20100
20108
  size = statSync13(path).size;
20101
20109
  }
20102
- const { text } = readRange(path, off, max);
20103
- const cut = text.lastIndexOf("\n");
20104
- const whole = cut < 0 ? "" : text.slice(0, cut + 1);
20110
+ const { text: text2 } = readRange(path, off, max);
20111
+ const cut = text2.lastIndexOf("\n");
20112
+ const whole = cut < 0 ? "" : text2.slice(0, cut + 1);
20105
20113
  const st = statSync13(path);
20106
20114
  return {
20107
20115
  lines: whole.split("\n").filter((l) => l.length),
@@ -20110,7 +20118,7 @@ function readTail(live, opts) {
20110
20118
  };
20111
20119
  }
20112
20120
  function waitForChange(dir, ms3, signal) {
20113
- return new Promise((resolve6) => {
20121
+ return new Promise((resolve7) => {
20114
20122
  let w;
20115
20123
  const done = () => {
20116
20124
  clearTimeout(t);
@@ -20120,7 +20128,7 @@ function waitForChange(dir, ms3, signal) {
20120
20128
  w?.close();
20121
20129
  } catch {
20122
20130
  }
20123
- resolve6();
20131
+ resolve7();
20124
20132
  };
20125
20133
  const t = setTimeout(done, ms3);
20126
20134
  const poll = setInterval(done, 500);
@@ -20267,13 +20275,13 @@ function send(res, r) {
20267
20275
  }
20268
20276
  async function socketIsStale(path) {
20269
20277
  if (!existsSync40(path)) return false;
20270
- return new Promise((resolve6) => {
20278
+ return new Promise((resolve7) => {
20271
20279
  const c = createConnection(path);
20272
20280
  c.once("connect", () => {
20273
20281
  c.destroy();
20274
- resolve6(false);
20282
+ resolve7(false);
20275
20283
  });
20276
- c.once("error", () => resolve6(true));
20284
+ c.once("error", () => resolve7(true));
20277
20285
  });
20278
20286
  }
20279
20287
  async function startControlServer(deps) {
@@ -20294,6 +20302,11 @@ async function startControlServer(deps) {
20294
20302
  res.on("close", () => abort.abort());
20295
20303
  void (async () => {
20296
20304
  try {
20305
+ const mcp = /^\/periscopes\/([^/]+)\/mcp\/?$/.exec(new URL(req.url ?? "/", "http://ship").pathname);
20306
+ if (mcp) {
20307
+ if (!deps.periscopeMcp) return send(res, err2(404, "not found"));
20308
+ return await deps.periscopeMcp(req, res, decodeURIComponent(mcp[1]));
20309
+ }
20297
20310
  const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
20298
20311
  const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
20299
20312
  if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
@@ -20307,10 +20320,10 @@ async function startControlServer(deps) {
20307
20320
  }
20308
20321
  })();
20309
20322
  });
20310
- return new Promise((resolve6) => {
20323
+ return new Promise((resolve7) => {
20311
20324
  server.once("error", (e) => {
20312
20325
  log(`control API not started: ${e.message}`);
20313
- resolve6(null);
20326
+ resolve7(null);
20314
20327
  });
20315
20328
  server.listen(path, () => {
20316
20329
  try {
@@ -20318,7 +20331,7 @@ async function startControlServer(deps) {
20318
20331
  } catch {
20319
20332
  }
20320
20333
  server.on("error", (e) => log(`control API error: ${e.message}`));
20321
- resolve6({
20334
+ resolve7({
20322
20335
  server,
20323
20336
  path,
20324
20337
  close: () => new Promise((done) => {
@@ -20349,20 +20362,434 @@ function schedulerHandler(control, run2 = runDetached) {
20349
20362
  };
20350
20363
  }
20351
20364
  function crewJson(args, timeoutMs = 6e4) {
20352
- return new Promise((resolve6) => {
20365
+ return new Promise((resolve7) => {
20353
20366
  execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
20354
20367
  try {
20355
20368
  const v = JSON.parse(String(stdout));
20356
- resolve6(v && typeof v === "object" && !Array.isArray(v) ? v : null);
20369
+ resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
20357
20370
  } catch {
20358
- resolve6(null);
20371
+ resolve7(null);
20359
20372
  }
20360
20373
  });
20361
20374
  });
20362
20375
  }
20363
20376
 
20377
+ // src/periscope-mcp.ts
20378
+ import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
20379
+ import { basename as basename13, dirname as dirname22, isAbsolute as isAbsolute3, join as join45, relative as relative2, resolve as resolve5, sep } from "path";
20380
+ import { z } from "zod";
20381
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20382
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20383
+
20384
+ // src/periscope-endpoints.ts
20385
+ import { randomBytes as randomBytes3 } from "crypto";
20386
+ import { chmodSync as chmodSync5, existsSync as existsSync41, mkdirSync as mkdirSync26, readdirSync as readdirSync18, readFileSync as readFileSync36, rmSync as rmSync19, writeFileSync as writeFileSync28 } from "fs";
20387
+ import { join as join44 } from "path";
20388
+ var secretDir = (stateDir) => join44(stateDir, "periscope-mcp");
20389
+ var secretFile = (stateDir, id) => join44(secretDir(stateDir), `${id}.json`);
20390
+ var endpointDir = (stateDir) => join44(stateDir, "periscope-endpoints");
20391
+ var endpointFile = (stateDir, id) => join44(endpointDir(stateDir), `${id}.json`);
20392
+ function mintPeriscopeSecret(stateDir, ticketRecordId) {
20393
+ mkdirSync26(secretDir(stateDir), { recursive: true, mode: 448 });
20394
+ const secret = randomBytes3(32).toString("base64url");
20395
+ const file = secretFile(stateDir, ticketRecordId);
20396
+ writeFileSync28(file, `${JSON.stringify({ secret })}
20397
+ `, { mode: 384 });
20398
+ chmodSync5(file, 384);
20399
+ return secret;
20400
+ }
20401
+ function readPeriscopeSecret(stateDir, ticketRecordId) {
20402
+ try {
20403
+ const v = JSON.parse(readFileSync36(secretFile(stateDir, ticketRecordId), "utf8"));
20404
+ return typeof v.secret === "string" && v.secret ? v.secret : void 0;
20405
+ } catch {
20406
+ return void 0;
20407
+ }
20408
+ }
20409
+ function removePeriscopeSecret(stateDir, ticketRecordId) {
20410
+ rmSync19(secretFile(stateDir, ticketRecordId), { force: true });
20411
+ }
20412
+ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
20413
+ try {
20414
+ return JSON.parse(readFileSync36(endpointFile(stateDir, ticketRecordId), "utf8"));
20415
+ } catch {
20416
+ return void 0;
20417
+ }
20418
+ }
20419
+ function writePeriscopeEndpoint(stateDir, rec) {
20420
+ mkdirSync26(endpointDir(stateDir), { recursive: true });
20421
+ writeFileSync28(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
20422
+ `);
20423
+ }
20424
+ function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
20425
+ rmSync19(endpointFile(stateDir, ticketRecordId), { force: true });
20426
+ }
20427
+ function listPeriscopeEndpoints(stateDir) {
20428
+ const dir = endpointDir(stateDir);
20429
+ if (!existsSync41(dir)) return [];
20430
+ const out = [];
20431
+ for (const name of readdirSync18(dir)) {
20432
+ if (!name.endsWith(".json")) continue;
20433
+ const rec = readPeriscopeEndpoint(stateDir, name.slice(0, -".json".length));
20434
+ if (rec && typeof rec.endpointId === "string") out.push(rec);
20435
+ }
20436
+ return out.sort((a, b) => a.registeredAt.localeCompare(b.registeredAt));
20437
+ }
20438
+ function periscopeEndpointUrl(relayUrl, ticket) {
20439
+ return `${relayUrl.replace(/\/+$/, "")}/control/periscopes/${encodeURIComponent(ticket)}/mcp`;
20440
+ }
20441
+ async function registerPeriscopeEndpoint(d, p) {
20442
+ const secret = readPeriscopeSecret(d.stateDir, p.ticketRecordId);
20443
+ if (!secret) return { ok: false, message: `${p.ticket} has no MCP secret (it was not opened with --mcp, or has ended)` };
20444
+ const api = d.apiFor(p.workspaceId);
20445
+ if (!api) return { ok: false, message: `no API key known for workspace ${p.workspaceId}` };
20446
+ const url = periscopeEndpointUrl(p.relayUrl, p.ticket);
20447
+ const stored = readPeriscopeEndpoint(d.stateDir, p.ticketRecordId);
20448
+ const out = await registerEndpoint(api, p.workspaceId, stored?.endpointId, {
20449
+ label: `Periscope: ${d.shipName} ${p.ticket}`,
20450
+ url,
20451
+ mcpSecret: secret
20452
+ });
20453
+ if (out.kind !== "ok") {
20454
+ return { ok: false, message: out.kind === "forbidden" ? "registering the endpoint was refused (403): the route key lacks REGISTER_MCP_ENDPOINTS" : out.message };
20455
+ }
20456
+ writePeriscopeEndpoint(d.stateDir, {
20457
+ ticket: p.ticket,
20458
+ ticketRecordId: p.ticketRecordId,
20459
+ route: p.route,
20460
+ workspaceId: p.workspaceId,
20461
+ endpointId: out.endpointId,
20462
+ url,
20463
+ registeredAt: (d.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
20464
+ });
20465
+ return { ok: true, endpointId: out.endpointId, url };
20466
+ }
20467
+ async function removeEndedPeriscopeEndpoints(d) {
20468
+ let removed = 0;
20469
+ for (const rec of listPeriscopeEndpoints(d.stateDir)) {
20470
+ if (readPeriscopeSecret(d.stateDir, rec.ticketRecordId)) continue;
20471
+ const api = d.apiFor(rec.workspaceId);
20472
+ if (!api) {
20473
+ d.warn(`no API key known for workspace ${rec.workspaceId} \u2014 cannot remove the MCP endpoint of ${rec.ticket}`);
20474
+ continue;
20475
+ }
20476
+ const out = await removeEndpoint(api, rec.workspaceId, rec.endpointId);
20477
+ if (out.kind === "ok") {
20478
+ removePeriscopeEndpointRecord(d.stateDir, rec.ticketRecordId);
20479
+ removed++;
20480
+ } else d.warn(`could not remove the MCP endpoint of ${rec.ticket}: ${out.kind === "forbidden" ? "403" : out.message}`);
20481
+ }
20482
+ return removed;
20483
+ }
20484
+
20485
+ // src/periscope-mcp.ts
20486
+ var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
20487
+ var MAX_READ_BYTES = 1024 * 1024;
20488
+ var MAX_WRITE_BYTES = 4 * 1024 * 1024;
20489
+ var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
20490
+ var MAX_HOOK_OUTPUT = 2e4;
20491
+ var MAX_DIFF = 2e5;
20492
+ var MAX_SEARCH_RESULTS = 500;
20493
+ var PathRefusal = class extends Error {
20494
+ };
20495
+ var isInside = (root, p) => {
20496
+ const rel = relative2(root, p);
20497
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute3(rel);
20498
+ };
20499
+ async function realAncestor(p) {
20500
+ let cur = p;
20501
+ const tail3 = [];
20502
+ let hops = 0;
20503
+ for (; ; ) {
20504
+ try {
20505
+ return { real: await realpath(cur), rest: tail3.slice().reverse().join(sep) };
20506
+ } catch (e) {
20507
+ if (e.code !== "ENOENT") throw new PathRefusal(`cannot resolve ${p}`);
20508
+ }
20509
+ let isLink = false;
20510
+ try {
20511
+ isLink = (await lstat(cur)).isSymbolicLink();
20512
+ } catch {
20513
+ }
20514
+ if (isLink) {
20515
+ if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
20516
+ cur = resolve5(dirname22(cur), await readlink(cur));
20517
+ continue;
20518
+ }
20519
+ const parent = dirname22(cur);
20520
+ if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
20521
+ tail3.push(basename13(cur));
20522
+ cur = parent;
20523
+ }
20524
+ }
20525
+ async function resolveInWorktree(root, requested) {
20526
+ if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
20527
+ const realRoot = await realpath(root);
20528
+ let abs;
20529
+ if (isAbsolute3(requested)) {
20530
+ abs = resolve5(requested);
20531
+ if (isInside(root, abs)) abs = join45(realRoot, relative2(root, abs));
20532
+ } else {
20533
+ abs = resolve5(realRoot, requested);
20534
+ }
20535
+ if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
20536
+ const { real, rest } = await realAncestor(abs);
20537
+ const final = rest ? join45(real, rest) : real;
20538
+ if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
20539
+ if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
20540
+ return final;
20541
+ }
20542
+ function globToRegExp(glob) {
20543
+ let re = "";
20544
+ for (let i = 0; i < glob.length; i++) {
20545
+ const c = glob[i];
20546
+ if (c === "*") {
20547
+ if (glob[i + 1] === "*") {
20548
+ i++;
20549
+ if (glob[i + 1] === "/") {
20550
+ i++;
20551
+ re += "(?:.*/)?";
20552
+ } else re += ".*";
20553
+ } else re += "[^/]*";
20554
+ } else if (c === "?") re += "[^/]";
20555
+ else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
20556
+ }
20557
+ return new RegExp(`^${re}$`);
20558
+ }
20559
+ var tailText = (s, max = MAX_HOOK_OUTPUT) => s.length > max ? `\u2026(${s.length - max} earlier characters omitted)
20560
+ ${s.slice(-max)}` : s;
20561
+ var text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...isError ? { isError: true } : {} });
20562
+ async function hookEnv(d, sc, name) {
20563
+ const env = { CREW_TICKET: sc.ticket, CREW_BRANCH: sc.branch };
20564
+ if (name !== "ports") {
20565
+ const ports = await d.runHook(sc, "ports", env);
20566
+ if (ports) Object.assign(env, parseEnvLines(ports.output));
20567
+ if (sc.isolated && name !== "isolate") {
20568
+ const iso = await d.runHook(sc, "isolate", env);
20569
+ if (iso && iso.code === 0) Object.assign(env, parseEnvLines(iso.output));
20570
+ }
20571
+ }
20572
+ if (name === "preview") env.CREW_PREVIEW_HOST = sc.host || "localhost";
20573
+ return env;
20574
+ }
20575
+ function createPeriscopeMcpServer(d, sc) {
20576
+ const server = new McpServer({ name: `crew-periscope-${sc.ticket}`, version: "0.1.0" });
20577
+ const root = sc.worktree;
20578
+ const inTree = (p) => resolveInWorktree(root, p);
20579
+ const rel = async (abs) => relative2(await realpath(root), abs) || ".";
20580
+ server.registerTool("read_file", {
20581
+ title: "Read File",
20582
+ description: "Read a text file in this periscope's worktree. `head`/`tail` return only the first/last N lines.",
20583
+ inputSchema: { path: z.string(), head: z.number().int().positive().optional(), tail: z.number().int().positive().optional() },
20584
+ annotations: { readOnlyHint: true, openWorldHint: false }
20585
+ }, async ({ path, head, tail: tail3 }) => {
20586
+ if (head && tail3) return text("Cannot specify both head and tail", true);
20587
+ const abs = await inTree(path);
20588
+ const st = await stat(abs);
20589
+ if (!st.isFile()) return text(`${path} is not a file`, true);
20590
+ if (st.size > MAX_READ_BYTES) return text(`${path} is ${st.size} bytes; read it in parts with head or tail (limit ${MAX_READ_BYTES})`, true);
20591
+ const content = await readFile(abs, "utf8");
20592
+ const lines = content.split("\n");
20593
+ return text(head ? lines.slice(0, head).join("\n") : tail3 ? lines.slice(-tail3).join("\n") : content);
20594
+ });
20595
+ server.registerTool("list_directory", {
20596
+ title: "List Directory",
20597
+ description: 'List the entries directly inside a directory of the worktree ("." is its root), each marked [FILE] or [DIR].',
20598
+ inputSchema: { path: z.string().default(".") },
20599
+ annotations: { readOnlyHint: true, openWorldHint: false }
20600
+ }, async ({ path }) => {
20601
+ const abs = await inTree(path);
20602
+ const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
20603
+ const lines = await Promise.all(entries.map(async (e) => {
20604
+ if (e.isDirectory()) return `[DIR] ${e.name}`;
20605
+ const size = (await lstat(join45(abs, e.name))).size;
20606
+ return `[FILE] ${e.name} (${size} bytes)`;
20607
+ }));
20608
+ return text(lines.join("\n") || "(empty)");
20609
+ });
20610
+ server.registerTool("search_files", {
20611
+ title: "Search Files",
20612
+ description: "Find files in the worktree whose path (relative to the worktree) matches the glob `pattern` (`*` stays within a directory, `**` crosses them). Lists files git tracks or would not ignore, so `node_modules` and build output do not appear.",
20613
+ inputSchema: { pattern: z.string() },
20614
+ annotations: { readOnlyHint: true, openWorldHint: false }
20615
+ }, async ({ pattern }) => {
20616
+ const re = globToRegExp(pattern);
20617
+ const listing = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
20618
+ const hits = listing.split("\0").filter((f) => f && re.test(f)).slice(0, MAX_SEARCH_RESULTS);
20619
+ return text(hits.length ? hits.join("\n") : "No matches found");
20620
+ });
20621
+ server.registerTool("write_file", {
20622
+ title: "Write File",
20623
+ description: "Create a file in the worktree, or replace its whole content. Parent directories are created.",
20624
+ inputSchema: { path: z.string(), content: z.string() },
20625
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
20626
+ }, async ({ path, content }) => {
20627
+ if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
20628
+ const abs = await inTree(path);
20629
+ if (abs === await realpath(root)) return text("path must name a file", true);
20630
+ await mkdir(dirname22(abs), { recursive: true });
20631
+ await inTree(path);
20632
+ await writeFile(abs, content);
20633
+ return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
20634
+ });
20635
+ server.registerTool("edit_file", {
20636
+ title: "Edit File",
20637
+ description: "Replace an exact string in a file. `oldString` must occur exactly once unless `replaceAll` is true; whitespace and line endings must match.",
20638
+ inputSchema: { path: z.string(), oldString: z.string().min(1), newString: z.string(), replaceAll: z.boolean().optional() },
20639
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
20640
+ }, async ({ path, oldString, newString, replaceAll }) => {
20641
+ const abs = await inTree(path);
20642
+ const st = await stat(abs);
20643
+ if (!st.isFile()) return text(`${path} is not a file`, true);
20644
+ if (st.size > MAX_WRITE_BYTES) return text(`${path} is too large to edit`, true);
20645
+ const before = await readFile(abs, "utf8");
20646
+ const count = before.split(oldString).length - 1;
20647
+ if (count === 0) return text(`oldString was not found in ${path}`, true);
20648
+ if (count > 1 && !replaceAll) return text(`oldString occurs ${count} times in ${path}; make it unique or set replaceAll`, true);
20649
+ const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, () => newString);
20650
+ await writeFile(abs, after);
20651
+ return text(`Edited ${await rel(abs)} (${replaceAll ? count : 1} replacement${(replaceAll ? count : 1) === 1 ? "" : "s"})`);
20652
+ });
20653
+ server.registerTool("create_directory", {
20654
+ title: "Create Directory",
20655
+ description: "Create a directory (and any missing parents) in the worktree.",
20656
+ inputSchema: { path: z.string() },
20657
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
20658
+ }, async ({ path }) => {
20659
+ const abs = await inTree(path);
20660
+ await mkdir(abs, { recursive: true });
20661
+ await inTree(path);
20662
+ return text(`Created ${await rel(abs)}`);
20663
+ });
20664
+ server.registerTool("delete_file", {
20665
+ title: "Delete File",
20666
+ description: "Delete one file (or symlink) from the worktree. Directories are not deleted.",
20667
+ inputSchema: { path: z.string() },
20668
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
20669
+ }, async ({ path }) => {
20670
+ const parent = await inTree(dirname22(path) === "" ? "." : dirname22(path));
20671
+ const abs = join45(parent, basename13(path));
20672
+ if (basename13(path) === ".git" || abs === parent) return text("path must name a file", true);
20673
+ const st = await lstat(abs).catch(() => void 0);
20674
+ if (!st) return text(`${path} does not exist`, true);
20675
+ if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
20676
+ await rm(abs, { force: false });
20677
+ return text(`Deleted ${await rel(abs)}`);
20678
+ });
20679
+ server.registerTool("run_hook", {
20680
+ title: "Run Hook",
20681
+ description: `Run one of the repository's declared hooks in this worktree, with this periscope's environment, and return its output. Allowed names: ${PERISCOPE_HOOKS.join(", ")} (\`isolate\` only for a periscope opened isolated). A hook the repository does not declare is refused.`,
20682
+ inputSchema: { name: z.enum(PERISCOPE_HOOKS) },
20683
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
20684
+ }, async ({ name }) => {
20685
+ if (name === "isolate" && !sc.isolated) return text("isolate is allowed only for a periscope opened with --isolate", true);
20686
+ const env = await hookEnv(d, sc, name);
20687
+ const res = await d.runHook(sc, name, env);
20688
+ if (!res) return text(`${sc.repo} declares no \`${name}\` hook`, true);
20689
+ return text(`exit ${res.code}
20690
+ ${tailText(res.output)}`, res.code !== 0);
20691
+ });
20692
+ const branchGuard = () => {
20693
+ const head = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
20694
+ return head === sc.branch ? void 0 : `the worktree is on ${head}, not the ticket branch ${sc.branch}`;
20695
+ };
20696
+ server.registerTool("git_status", {
20697
+ title: "Git Status",
20698
+ description: "Branch and changed files (`git status --short --branch`) of this worktree.",
20699
+ inputSchema: {},
20700
+ annotations: { readOnlyHint: true, openWorldHint: false }
20701
+ }, async () => text(git(root, ["status", "--short", "--branch", "--untracked-files=all"])));
20702
+ server.registerTool("git_diff", {
20703
+ title: "Git Diff",
20704
+ description: "The uncommitted changes against HEAD (staged and unstaged), optionally limited to one path. New untracked files appear in git_status, not here.",
20705
+ inputSchema: { path: z.string().optional() },
20706
+ annotations: { readOnlyHint: true, openWorldHint: false }
20707
+ }, async ({ path }) => {
20708
+ const args = ["diff", "HEAD", "--no-ext-diff", "--no-color"];
20709
+ if (path) args.push("--", relative2(await realpath(root), await inTree(path)) || ".");
20710
+ const out = git(root, args);
20711
+ return text(out ? tailText(out, MAX_DIFF) : "(no changes)");
20712
+ });
20713
+ server.registerTool("git_commit", {
20714
+ title: "Git Commit",
20715
+ description: "Stage every change in the worktree and commit it on the ticket branch. It never pushes; ending the periscope with `--keep` does.",
20716
+ inputSchema: { message: z.string().min(1).max(2e3) },
20717
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
20718
+ }, async ({ message }) => {
20719
+ const wrong = branchGuard();
20720
+ if (wrong) return text(wrong, true);
20721
+ git(root, ["add", "-A"]);
20722
+ try {
20723
+ git(root, ["commit", "--no-verify", "-m", message]);
20724
+ } catch (e) {
20725
+ if (e instanceof GitError && /nothing to commit|no changes added/.test(e.message)) return text("Nothing to commit", true);
20726
+ throw e;
20727
+ }
20728
+ return text(`Committed ${git(root, ["rev-parse", "--short", "HEAD"])} on ${sc.branch}`);
20729
+ });
20730
+ return server;
20731
+ }
20732
+ var NOT_FOUND = { error: "not found" };
20733
+ var json = (res, status2, body) => {
20734
+ const b = JSON.stringify(body);
20735
+ res.writeHead(status2, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(b), "Cache-Control": "no-store" });
20736
+ res.end(b);
20737
+ };
20738
+ async function readBody(req) {
20739
+ const chunks = [];
20740
+ let n = 0;
20741
+ for await (const c of req) {
20742
+ n += c.length;
20743
+ if (n > MAX_BODY_BYTES2) return null;
20744
+ chunks.push(c);
20745
+ }
20746
+ return Buffer.concat(chunks).toString("utf8");
20747
+ }
20748
+ async function handlePeriscopeMcp(d, req, res, segment) {
20749
+ const sc = d.find(segment);
20750
+ const secret = sc ? readPeriscopeSecret(d.stateDir, sc.ticketRecordId) : void 0;
20751
+ const presented = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
20752
+ if (!sc || !secret || !presented || !secretsMatch(presented, secret)) return json(res, 404, NOT_FOUND);
20753
+ if (req.method !== "POST") {
20754
+ res.setHeader("Allow", "POST");
20755
+ return json(res, 405, { jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed." }, id: null });
20756
+ }
20757
+ const raw = await readBody(req);
20758
+ if (raw === null) return json(res, 413, { jsonrpc: "2.0", error: { code: -32600, message: "request body too large" }, id: null });
20759
+ let parsed;
20760
+ try {
20761
+ parsed = raw ? JSON.parse(raw) : void 0;
20762
+ } catch {
20763
+ return json(res, 400, { jsonrpc: "2.0", error: { code: -32700, message: "parse error" }, id: null });
20764
+ }
20765
+ const isCall = (m) => !!m && typeof m === "object" && m.method === "tools/call";
20766
+ if (Array.isArray(parsed) ? parsed.some(isCall) : isCall(parsed)) await d.touch(sc).catch((e) => d.log?.(`periscope ${sc.ticket} activity not recorded: ${e.message}`));
20767
+ try {
20768
+ const server = createPeriscopeMcpServer(d, sc);
20769
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
20770
+ await server.connect(transport);
20771
+ res.on("close", () => {
20772
+ void transport.close();
20773
+ void server.close();
20774
+ });
20775
+ await transport.handleRequest(req, res, parsed);
20776
+ } catch (e) {
20777
+ if (!res.headersSent) json(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: e.message || "internal error" }, id: null });
20778
+ }
20779
+ }
20780
+ function findPeriscope(sidecars, segment) {
20781
+ const m = /^(?:([A-Za-z]+)-)?(\d+)$/.exec(segment);
20782
+ if (!m) return void 0;
20783
+ const num = String(Number(m[2]));
20784
+ return sidecars.find((sc) => {
20785
+ const t = /^(?:([A-Za-z]+)-)?0*(\d+)$/.exec(sc.ticket);
20786
+ if (!t || t[2] !== num) return false;
20787
+ return !m[1] || !t[1] || m[1].toLowerCase() === t[1].toLowerCase();
20788
+ });
20789
+ }
20790
+
20364
20791
  // src/cli.ts
20365
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname22(process.execPath) : resolve5(dirname22(fileURLToPath(import.meta.url)), "..");
20792
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname23(process.execPath) : resolve6(dirname23(fileURLToPath(import.meta.url)), "..");
20366
20793
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
20367
20794
  var CYCLE_SECONDS2 = 120;
20368
20795
  var STALE_CYCLES = 3;
@@ -20502,7 +20929,7 @@ No project here has what a route needs. Install the "${template.name}" library t
20502
20929
  return installed.project.slug;
20503
20930
  }
20504
20931
  function usage(problem, requested = false) {
20505
- const text = `crew \u2014 a standing team of headless agents
20932
+ const text2 = `crew \u2014 a standing team of headless agents
20506
20933
 
20507
20934
  crew poll [route] decide a cycle and report it; writes nothing
20508
20935
  crew run [route] [--role R] run the winning role's session
@@ -20528,7 +20955,7 @@ function usage(problem, requested = false) {
20528
20955
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
20529
20956
  crew drop [route] NNN remove a merged ticket's worktree and branch
20530
20957
  crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
20531
- crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
20958
+ crew periscope up [[route] REPO] [--topic TEXT|--issue NNN] [--isolate] [--serve-mcp] [--host H] [--assignee ID] [--no-open] [--open] [--dry-run] [--json]
20532
20959
  crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
20533
20960
  file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
20534
20961
  crew periscope down [[route] NNN] --keep|--discard [--allow-empty] [--json]
@@ -20581,12 +21008,12 @@ Options:
20581
21008
  In watch: space pauses the stream, q quits.
20582
21009
  `;
20583
21010
  if (requested) {
20584
- process.stdout.write(text);
21011
+ process.stdout.write(text2);
20585
21012
  process.exit(0);
20586
21013
  }
20587
21014
  process.stderr.write(problem ? `crew: ${problem}
20588
21015
 
20589
- ${text}` : text);
21016
+ ${text2}` : text2);
20590
21017
  process.exit(2);
20591
21018
  }
20592
21019
  var argv = process.argv.slice(2);
@@ -20759,6 +21186,44 @@ if (command === "passenger-sync-daemon") {
20759
21186
  return void 0;
20760
21187
  }
20761
21188
  };
21189
+ }, periscopeEndpointDeps = function() {
21190
+ return {
21191
+ stateDir: cfg.ship.stateDir,
21192
+ shipName: cfg.ship.name,
21193
+ apiFor: endpointApiFor(cfg.routes.filter((r) => r.enabled)),
21194
+ warn: (m) => emit.warn(m, { step: "passengers" })
21195
+ };
21196
+ }, periscopeMcpDeps = function() {
21197
+ const stateDir = cfg.ship.stateDir;
21198
+ return {
21199
+ stateDir,
21200
+ find: (segment) => findPeriscope(readPeriscopeSidecars(stateDir), segment),
21201
+ runHook: async (sc, name, env) => {
21202
+ const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
21203
+ const repo = rt && (await resolvedRepos(rt)).find((r) => r.name === sc.repo);
21204
+ const script = repo?.config.hooks[name];
21205
+ if (!repo || !script) return null;
21206
+ return runScript(script, {
21207
+ cwd: sc.worktree,
21208
+ env,
21209
+ shell: resolveShell(repo.config.shell ?? cfg.ship.shell),
21210
+ timeoutMs: name === "preview" ? 18e4 : 15 * 6e4
21211
+ });
21212
+ },
21213
+ touch: async (sc) => {
21214
+ const now = Date.now();
21215
+ if (now - (periscopeTouched.get(sc.ticketRecordId) ?? 0) < PERISCOPE_TOUCH_EVERY_MS) return;
21216
+ periscopeTouched.set(sc.ticketRecordId, now);
21217
+ const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
21218
+ if (!rt) return;
21219
+ const { idleMinutes } = periscopeCaps(cfg.ship);
21220
+ const at = new Date(now).toISOString();
21221
+ const expiresAt = new Date(now + idleMinutes * 6e4).toISOString();
21222
+ await new Tracker(rt, cfg.ship).updatePeriscope(sc.periscopeRecordId, { last_activity_at: at, expires_at: expiresAt });
21223
+ writePeriscopeSidecar(stateDir, { ...sc, lastActivityAt: at, expiresAt });
21224
+ },
21225
+ log: (m) => emit.warn(m, { step: "passengers" })
21226
+ };
20762
21227
  }, endpointReconcileDeps = function(routes, relayHost) {
20763
21228
  return {
20764
21229
  stateDir: cfg.ship.stateDir,
@@ -20785,7 +21250,7 @@ if (command === "passenger-sync-daemon") {
20785
21250
  const unsafe = baseBranchUnsafe(out);
20786
21251
  return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
20787
21252
  };
20788
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
21253
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
20789
21254
  let cfg;
20790
21255
  let firstRun;
20791
21256
  let firstRunFromFlags = false;
@@ -20841,7 +21306,7 @@ if (command === "passenger-sync-daemon") {
20841
21306
  } else {
20842
21307
  const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
20843
21308
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
20844
- if (configless && !existsSync41(findConfigFile(CREW_HOME))) {
21309
+ if (configless && !existsSync42(findConfigFile(CREW_HOME))) {
20845
21310
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
20846
21311
  } else {
20847
21312
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -20856,7 +21321,7 @@ if (command === "passenger-sync-daemon") {
20856
21321
  throw e;
20857
21322
  }
20858
21323
  if (command === "credential") {
20859
- const r = runCredential(positional[1], readFileSync36(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
21324
+ const r = runCredential(positional[1], readFileSync37(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
20860
21325
  process.stdout.write(r.out);
20861
21326
  process.stderr.write(r.err);
20862
21327
  process.exit(r.code);
@@ -20868,9 +21333,9 @@ if (command === "passenger-sync-daemon") {
20868
21333
  forges: cfg.ship.forges,
20869
21334
  stateDir: cfg.ship.stateDir,
20870
21335
  configFile: cfg.configFile,
20871
- readFile: (p) => readFileSync36(p, "utf8"),
20872
- writeFile: (p, t) => writeFileSync28(p, t),
20873
- stdin: () => readFileSync36(0, "utf8"),
21336
+ readFile: (p) => readFileSync37(p, "utf8"),
21337
+ writeFile: (p, t) => writeFileSync29(p, t),
21338
+ stdin: () => readFileSync37(0, "utf8"),
20874
21339
  env: process.env
20875
21340
  }
20876
21341
  );
@@ -21002,6 +21467,7 @@ if (command === "passenger-sync-daemon") {
21002
21467
  for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
21003
21468
  for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
21004
21469
  await Promise.all(pending);
21470
+ await reconcilePeriscopeEndpoints();
21005
21471
  } catch (e) {
21006
21472
  emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
21007
21473
  }
@@ -21021,6 +21487,22 @@ if (command === "passenger-sync-daemon") {
21021
21487
  emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
21022
21488
  }
21023
21489
  }
21490
+ const periscopeTouched = /* @__PURE__ */ new Map();
21491
+ const PERISCOPE_TOUCH_EVERY_MS = 3e4;
21492
+ async function reconcilePeriscopeEndpoints() {
21493
+ const d = periscopeEndpointDeps();
21494
+ await removeEndedPeriscopeEndpoints(d);
21495
+ const have = new Set(listPeriscopeEndpoints(cfg.ship.stateDir).map((e) => e.ticketRecordId));
21496
+ for (const sc of readPeriscopeSidecars(cfg.ship.stateDir)) {
21497
+ if (have.has(sc.ticketRecordId) || !sc.route || !readPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId)) continue;
21498
+ const rt = cfg.routes.find((r) => r.route === sc.route && r.enabled);
21499
+ const ws = rt?.resolved?.workspaceId;
21500
+ const relayUrl = rt && routeRelayUrl(rt);
21501
+ if (!rt || !ws || !relayUrl) continue;
21502
+ const out = await registerPeriscopeEndpoint(d, { ticket: sc.ticket, ticketRecordId: sc.ticketRecordId, route: sc.route, workspaceId: ws, relayUrl });
21503
+ if (!out.ok) d.warn(`could not register the MCP endpoint of ${sc.ticket}: ${out.message}`);
21504
+ }
21505
+ }
21024
21506
  async function releaseFleet(opts = {}) {
21025
21507
  if (releaseRefused()) return;
21026
21508
  precheckTickets.clear();
@@ -21136,8 +21618,8 @@ if (command === "passenger-sync-daemon") {
21136
21618
  }
21137
21619
  if (host === "linux" && hasSystemd) {
21138
21620
  const label = labelFor(CREW_HOME, "run");
21139
- const staleTimer = join44(homedir10(), ".config", "systemd", "user", `${label}.timer`);
21140
- if (existsSync41(staleTimer)) {
21621
+ const staleTimer = join46(homedir10(), ".config", "systemd", "user", `${label}.timer`);
21622
+ if (existsSync42(staleTimer)) {
21141
21623
  process.stdout.write(
21142
21624
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
21143
21625
  `
@@ -21164,7 +21646,7 @@ if (command === "passenger-sync-daemon") {
21164
21646
  const tracker = new Tracker(c, cfg.ship);
21165
21647
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
21166
21648
  for (const r of await resolvedRepos(c)) {
21167
- if (!existsSync41(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
21649
+ if (!existsSync42(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
21168
21650
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
21169
21651
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
21170
21652
  }
@@ -21182,7 +21664,7 @@ if (command === "passenger-sync-daemon") {
21182
21664
  }
21183
21665
  async function doctorFix(o) {
21184
21666
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
21185
- const unitExists = (p) => existsSync41(p);
21667
+ const unitExists = (p) => existsSync42(p);
21186
21668
  const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
21187
21669
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
21188
21670
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -21199,7 +21681,7 @@ if (command === "passenger-sync-daemon") {
21199
21681
  for (const a of plan.actions) {
21200
21682
  if (a.kind === "enable") {
21201
21683
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
21202
- writeFileSync28(cfg.configFile, planEnable(readFileSync36(cfg.configFile, "utf8"), a.route));
21684
+ writeFileSync29(cfg.configFile, planEnable(readFileSync37(cfg.configFile, "utf8"), a.route));
21203
21685
  enabledSound.add(a.route);
21204
21686
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
21205
21687
  } else {
@@ -22320,7 +22802,7 @@ ${cu.tail.trimEnd()}
22320
22802
  process.exit(1);
22321
22803
  }
22322
22804
  requireArmed("run the daemon loop");
22323
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join44(CREW_HOME, "src"), join44(CREW_HOME, "dist"), join44(CREW_HOME, "bin")];
22805
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join46(CREW_HOME, "src"), join46(CREW_HOME, "dist"), join46(CREW_HOME, "bin")];
22324
22806
  const configPaths = daemonConfigPaths(cfg);
22325
22807
  watchPaths.push(...configPaths);
22326
22808
  noteDaemonStarted(cfg.ship.stateDir);
@@ -22348,8 +22830,22 @@ ${cu.tail.trimEnd()}
22348
22830
  previewHost: cfg.ship.previewHost,
22349
22831
  maxPeriscopes: cfg.ship.maxPeriscopes,
22350
22832
  run: runCrew,
22351
- verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
22833
+ verifyIn: (token, ws) => controlAuth.verifyIn(token, ws),
22834
+ mcp: {
22835
+ onUp: async (r, done) => {
22836
+ const id = typeof done.ticketRecordId === "string" ? done.ticketRecordId : "";
22837
+ const ticket = typeof done.ticket === "string" ? done.ticket : "";
22838
+ const relayUrl = routeRelayUrl(cfg.routes.find((x) => x.route === r.route));
22839
+ if (!relayUrl) return { mcpEndpointId: null, mcpWarning: "this ship has no relay URL yet, so the periscope's MCP endpoint is not registered; it is retried each cycle" };
22840
+ const out = await registerPeriscopeEndpoint(periscopeEndpointDeps(), { ticket, ticketRecordId: id, route: r.route, workspaceId: r.workspaceId, relayUrl });
22841
+ return out.ok ? { mcpEndpointId: out.endpointId, mcpUrl: out.url } : { mcpEndpointId: null, mcpWarning: out.message };
22842
+ },
22843
+ onDown: async () => {
22844
+ await removeEndedPeriscopeEndpoints(periscopeEndpointDeps());
22845
+ }
22846
+ }
22352
22847
  },
22848
+ periscopeMcp: controlRoutes.some((r) => r.enabled && r.hostPeriscopes) ? (req, res, ticket) => handlePeriscopeMcp(periscopeMcpDeps(), req, res, ticket) : void 0,
22353
22849
  status: () => crewJson(["status", route2.route, "--json"]),
22354
22850
  doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
22355
22851
  scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
@@ -22581,10 +23077,10 @@ ${cu.tail.trimEnd()}
22581
23077
  `);
22582
23078
  if (!dryRun) {
22583
23079
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
22584
- const raw = existsSync41(resolvedPath) ? JSON.parse(readFileSync36(resolvedPath, "utf8")) : {};
23080
+ const raw = existsSync42(resolvedPath) ? JSON.parse(readFileSync37(resolvedPath, "utf8")) : {};
22585
23081
  raw.agentSkills = result.agentSkills;
22586
- mkdirSync26(dirOf2(resolvedPath), { recursive: true });
22587
- writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
23082
+ mkdirSync27(dirOf2(resolvedPath), { recursive: true });
23083
+ writeFileSync29(resolvedPath, `${JSON.stringify(raw, null, 2)}
22588
23084
  `);
22589
23085
  }
22590
23086
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -22607,7 +23103,7 @@ ${cu.tail.trimEnd()}
22607
23103
  throw e;
22608
23104
  }
22609
23105
  const oldName = cfg.ship.name;
22610
- writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), newName));
23106
+ writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), newName));
22611
23107
  cfg.ship.name = newName;
22612
23108
  let renameMachineId;
22613
23109
  try {
@@ -22654,14 +23150,14 @@ ${cu.tail.trimEnd()}
22654
23150
  process.exit(2);
22655
23151
  };
22656
23152
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
22657
- const configText = readFileSync36(cfg.configFile, "utf8");
23153
+ const configText = readFileSync37(cfg.configFile, "utf8");
22658
23154
  if (sub === "check") {
22659
23155
  try {
22660
23156
  const routeArg = value("route") ?? positional[2];
22661
23157
  const found = cfg.routes.find((r) => r.route === routeArg);
22662
23158
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
22663
23159
  const named2 = value("route") ? void 0 : positional[3];
22664
- const dir = named2 ? repoDirOf(configText, dirname22(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
23160
+ const dir = named2 ? repoDirOf(configText, dirname23(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
22665
23161
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
22666
23162
  const o = target ? found.repoOverrides[target.name] : void 0;
22667
23163
  runReposCheck(dir, {
@@ -22684,17 +23180,17 @@ ${cu.tail.trimEnd()}
22684
23180
  if (sub === "hooks") {
22685
23181
  try {
22686
23182
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
22687
- const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
22688
- if (!existsSync41(dir)) throw new ReposError(`${dir} does not exist on disk`);
22689
- const files = { exists: (r) => existsSync41(resolvePath(dir, r)), list: (r) => {
23183
+ const dir = repoDirOf(configText, dirname23(cfg.configFile), a.route, a.name);
23184
+ if (!existsSync42(dir)) throw new ReposError(`${dir} does not exist on disk`);
23185
+ const files = { exists: (r) => existsSync42(resolvePath(dir, r)), list: (r) => {
22690
23186
  try {
22691
- return readdirSync18(resolvePath(dir, r));
23187
+ return readdirSync19(resolvePath(dir, r));
22692
23188
  } catch {
22693
23189
  return [];
22694
23190
  }
22695
23191
  }, read: (r) => {
22696
23192
  try {
22697
- return readFileSync36(resolvePath(dir, r), "utf8");
23193
+ return readFileSync37(resolvePath(dir, r), "utf8");
22698
23194
  } catch {
22699
23195
  return void 0;
22700
23196
  }
@@ -22736,19 +23232,19 @@ ${cu.tail.trimEnd()}
22736
23232
  let cleared;
22737
23233
  if (clear2) {
22738
23234
  const plan = clearRouteHooks(configText, a.route, a.name);
22739
- if (plan.removed.length > 0) writeFileSync28(cfg.configFile, plan.text);
23235
+ if (plan.removed.length > 0) writeFileSync29(cfg.configFile, plan.text);
22740
23236
  cleared = { file: cfg.configFile, removed: plan.removed };
22741
23237
  }
22742
23238
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
22743
23239
  if (write === "route") {
22744
23240
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
22745
- writeFileSync28(cfg.configFile, plan.text);
23241
+ writeFileSync29(cfg.configFile, plan.text);
22746
23242
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
22747
23243
  } else if (write === "repo") {
22748
23244
  const have = existingRepoConfig(files);
22749
23245
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
22750
23246
  const file = resolvePath(dir, ".crew.yaml");
22751
- writeFileSync28(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
23247
+ writeFileSync29(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
22752
23248
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
22753
23249
  }
22754
23250
  if (flag("json")) {
@@ -22804,7 +23300,7 @@ ${cu.tail.trimEnd()}
22804
23300
  if (sub === "list" && !routeName) usage2();
22805
23301
  if (sub === "add") {
22806
23302
  try {
22807
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync41(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
23303
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync42(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
22808
23304
  routeName = a.route;
22809
23305
  addPath = a.path;
22810
23306
  addName = a.name;
@@ -22833,66 +23329,66 @@ ${cu.tail.trimEnd()}
22833
23329
  }
22834
23330
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
22835
23331
  let resolvedRemotes;
22836
- if (existsSync41(resolvedFile)) {
23332
+ if (existsSync42(resolvedFile)) {
22837
23333
  try {
22838
- resolvedRemotes = JSON.parse(readFileSync36(resolvedFile, "utf8"));
23334
+ resolvedRemotes = JSON.parse(readFileSync37(resolvedFile, "utf8"));
22839
23335
  } catch {
22840
23336
  }
22841
23337
  }
22842
23338
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
22843
- const text = configText;
22844
- const base = dirname22(cfg.configFile);
22845
- const isCheckout = (d) => existsSync41(`${d}/.git`);
23339
+ const text2 = configText;
23340
+ const base = dirname23(cfg.configFile);
23341
+ const isCheckout = (d) => existsSync42(`${d}/.git`);
22846
23342
  try {
22847
23343
  if (sub === "list") {
22848
23344
  if (flag("json")) {
22849
23345
  const rows = listRepoRows({
22850
- text,
23346
+ text: text2,
22851
23347
  route: routeName,
22852
23348
  base,
22853
23349
  resolved: resolvedRemotes,
22854
23350
  originOf,
22855
- exists: (d) => existsSync41(d),
23351
+ exists: (d) => existsSync42(d),
22856
23352
  isCheckout,
22857
23353
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
22858
23354
  try {
22859
- return Object.keys(parseRepoConfig(readFileSync36(resolvePath(d, f), "utf8"), f).hooks).length > 0;
23355
+ return Object.keys(parseRepoConfig(readFileSync37(resolvePath(d, f), "utf8"), f).hooks).length > 0;
22860
23356
  } catch {
22861
23357
  return false;
22862
23358
  }
22863
23359
  })
22864
23360
  });
22865
- process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(text, routeName, base, isCheckout) })}
23361
+ process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(text2, routeName, base, isCheckout) })}
22866
23362
  `);
22867
23363
  break;
22868
23364
  }
22869
- const cloneBase = cloneBaseOf(text, routeName, base, isCheckout);
22870
- const repos = configuredRepos(text, routeName, base).filter((r) => r.dir !== cloneBase);
23365
+ const cloneBase = cloneBaseOf(text2, routeName, base, isCheckout);
23366
+ const repos = configuredRepos(text2, routeName, base).filter((r) => r.dir !== cloneBase);
22871
23367
  if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
22872
23368
  `);
22873
23369
  for (const r of repos) {
22874
23370
  const origin2 = originOf(r.dir);
22875
23371
  const row = matchRepoName(origin2, resolvedRemotes);
22876
- const note = !existsSync41(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
23372
+ const note = !existsSync42(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
22877
23373
  process.stdout.write(`${r.name} ${r.dir} ${note}
22878
23374
  `);
22879
23375
  }
22880
23376
  break;
22881
23377
  }
22882
23378
  if (sub === "clone") {
22883
- const json = flag("json");
22884
- const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
23379
+ const json2 = flag("json");
23380
+ const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
22885
23381
  ` : `${human}
22886
23382
  `);
22887
- const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
22888
- if (existsSync41(plan2.target) && (existsSync41(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
23383
+ const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
23384
+ if (existsSync42(plan2.target) && (existsSync42(`${plan2.target}/.git`) || readdirSync19(plan2.target).length > 0)) {
22889
23385
  throw new ReposError(
22890
- existsSync41(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
23386
+ existsSync42(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
22891
23387
  );
22892
23388
  }
22893
23389
  if (dryRun) {
22894
23390
  say("dry-run", { remote: plan2.remote, target: plan2.target }, `would clone ${plan2.remote} into ${plan2.target}`);
22895
- if (!json) process.stdout.write(`${plan2.block}
23391
+ if (!json2) process.stdout.write(`${plan2.block}
22896
23392
  `);
22897
23393
  break;
22898
23394
  }
@@ -22905,18 +23401,18 @@ ${cu.tail.trimEnd()}
22905
23401
  throw new ReposError(`${e.message}${authHint}
22906
23402
  crew.yaml was not changed`);
22907
23403
  }
22908
- writeFileSync28(cfg.configFile, plan2.text);
23404
+ writeFileSync29(cfg.configFile, plan2.text);
22909
23405
  if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
22910
23406
  `);
22911
23407
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
22912
23408
  break;
22913
23409
  }
22914
23410
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
22915
- const top = existsSync41(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
23411
+ const top = existsSync42(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
22916
23412
  if (!top) throw new ReposError(`${target} is not a git checkout`);
22917
23413
  const origin = originOf(top);
22918
23414
  const plan = planRepoAdd({
22919
- text,
23415
+ text: text2,
22920
23416
  base,
22921
23417
  route: routeName,
22922
23418
  path: top,
@@ -22964,15 +23460,15 @@ ${cu.tail.trimEnd()}
22964
23460
  const created = await rowTracker.createRepoRow(rowPayload);
22965
23461
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
22966
23462
  `);
22967
- if (existsSync41(resolvedFile)) {
22968
- const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
23463
+ if (existsSync42(resolvedFile)) {
23464
+ const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
22969
23465
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
22970
23466
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
22971
- writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
23467
+ writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
22972
23468
  `);
22973
23469
  }
22974
23470
  }
22975
- writeFileSync28(cfg.configFile, plan.text);
23471
+ writeFileSync29(cfg.configFile, plan.text);
22976
23472
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
22977
23473
  `);
22978
23474
  } catch (e) {
@@ -23339,7 +23835,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23339
23835
  process.exit(2);
23340
23836
  }
23341
23837
  if (found.shipNameAdopted && !dryRun) {
23342
- writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), found.shipNameAdopted));
23838
+ writeFileSync29(cfg.configFile, setShipNameInYaml(readFileSync37(cfg.configFile, "utf8"), found.shipNameAdopted));
23343
23839
  cfg.ship.name = found.shipNameAdopted;
23344
23840
  }
23345
23841
  if (found.shipRow) {
@@ -23382,10 +23878,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23382
23878
  }
23383
23879
  }
23384
23880
  let previous;
23385
- const hadPreviousFile = existsSync41(resolvedPath);
23881
+ const hadPreviousFile = existsSync42(resolvedPath);
23386
23882
  if (hadPreviousFile) {
23387
23883
  try {
23388
- previous = JSON.parse(readFileSync36(resolvedPath, "utf8"));
23884
+ previous = JSON.parse(readFileSync37(resolvedPath, "utf8"));
23389
23885
  } catch {
23390
23886
  }
23391
23887
  }
@@ -23515,10 +24011,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23515
24011
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
23516
24012
  `);
23517
24013
  } else {
23518
- mkdirSync26(dirname22(resolvedPath), { recursive: true });
24014
+ mkdirSync27(dirname23(resolvedPath), { recursive: true });
23519
24015
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
23520
24016
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
23521
- writeFileSync28(tmpPath, `${JSON.stringify(resolved, null, 2)}
24017
+ writeFileSync29(tmpPath, `${JSON.stringify(resolved, null, 2)}
23522
24018
  `);
23523
24019
  renameSync4(tmpPath, resolvedPath);
23524
24020
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -23564,8 +24060,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23564
24060
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
23565
24061
  `);
23566
24062
  } else {
23567
- mkdirSync26(dirname22(apiKeyFile), { recursive: true });
23568
- writeFileSync28(apiKeyFile, `${apiKeyVar}=${apiKey}
24063
+ mkdirSync27(dirname23(apiKeyFile), { recursive: true });
24064
+ writeFileSync29(apiKeyFile, `${apiKeyVar}=${apiKey}
23569
24065
  `, { mode: 384 });
23570
24066
  process.stderr.write(`API key written to ${apiKeyFile}
23571
24067
  `);
@@ -24252,6 +24748,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24252
24748
  },
24253
24749
  setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
24254
24750
  forgetLocal: (id) => {
24751
+ removePeriscopeSecret(cfg.ship.stateDir, id);
24255
24752
  removePeriscopeSidecar(cfg.ship.stateDir, id);
24256
24753
  removePeriscopePrior(cfg.ship.stateDir, id);
24257
24754
  },
@@ -24300,7 +24797,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24300
24797
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
24301
24798
  const resumed = await runPeriscopeResume({
24302
24799
  find: async () => hit && { row: hit.row, tag: hit.tag },
24303
- worktreeExists: (p) => existsSync41(p),
24800
+ worktreeExists: (p) => existsSync42(p),
24304
24801
  caps: periscopeCaps(cfg.ship),
24305
24802
  runHook: async (name, cwd, env) => {
24306
24803
  const script = hit?.repo.config.hooks[name];
@@ -24394,10 +24891,10 @@ ${where.join("\n")}`);
24394
24891
  },
24395
24892
  prepare: async (path) => {
24396
24893
  for (const f of repoP.config.worktrees.copy) {
24397
- const src = join44(repoP.dir, f);
24398
- if (!existsSync41(src)) continue;
24399
- mkdirSync26(dirOf2(join44(path, f)), { recursive: true });
24400
- copyFileSync2(src, join44(path, f));
24894
+ const src = join46(repoP.dir, f);
24895
+ if (!existsSync42(src)) continue;
24896
+ mkdirSync27(dirOf2(join46(path, f)), { recursive: true });
24897
+ copyFileSync2(src, join46(path, f));
24401
24898
  }
24402
24899
  const setup = repoP.config.hooks.setup;
24403
24900
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -24409,7 +24906,10 @@ ${where.join("\n")}`);
24409
24906
  return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
24410
24907
  },
24411
24908
  createRow: (fields) => trackerP.createPeriscope(fields),
24412
- recordLocal: (sc) => writePeriscopeSidecar(cfg.ship.stateDir, sc),
24909
+ recordLocal: (sc) => {
24910
+ writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
24911
+ if (flag("serve-mcp")) mintPeriscopeSecret(cfg.ship.stateDir, sc.ticketRecordId);
24912
+ },
24413
24913
  comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
24414
24914
  teardown: async (path, branch) => {
24415
24915
  const n = Number(path.match(/(\d+)\/?$/)?.[1]);
@@ -24687,7 +25187,7 @@ ${where.join("\n")}`);
24687
25187
  };
24688
25188
  const droppedRoutes = [];
24689
25189
  try {
24690
- const rawCfg = parseYaml2(readFileSync36(cfg.configFile, "utf8"));
25190
+ const rawCfg = parseYaml2(readFileSync37(cfg.configFile, "utf8"));
24691
25191
  for (const r of rawCfg?.routes ?? []) {
24692
25192
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
24693
25193
  try {
@@ -24729,8 +25229,8 @@ ${where.join("\n")}`);
24729
25229
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
24730
25230
  break;
24731
25231
  }
24732
- mkdirSync26(cfg.ship.stateDir, { recursive: true });
24733
- writeFileSync28(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
25232
+ mkdirSync27(cfg.ship.stateDir, { recursive: true });
25233
+ writeFileSync29(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
24734
25234
  `);
24735
25235
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
24736
25236
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -24781,8 +25281,8 @@ ${where.join("\n")}`);
24781
25281
  }
24782
25282
  case "log": {
24783
25283
  try {
24784
- const text = readFileSync36(cfg.ship.logFile, "utf8").split("\n");
24785
- process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
25284
+ const text2 = readFileSync37(cfg.ship.logFile, "utf8").split("\n");
25285
+ process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
24786
25286
  `);
24787
25287
  } catch {
24788
25288
  process.stdout.write(`no log yet at ${cfg.ship.logFile}
@@ -24791,12 +25291,12 @@ ${where.join("\n")}`);
24791
25291
  break;
24792
25292
  }
24793
25293
  case "sample-ticket": {
24794
- const json = flag("json");
24795
- const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
25294
+ const json2 = flag("json");
25295
+ const say = (event, extra, human) => process.stdout.write(json2 ? `${JSON.stringify({ event, ...extra })}
24796
25296
  ` : `${human}
24797
25297
  `);
24798
25298
  const fail = (msg) => {
24799
- if (json) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
25299
+ if (json2) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
24800
25300
  `);
24801
25301
  else process.stderr.write(`crew sample-ticket: ${msg}
24802
25302
  `);
@@ -24804,8 +25304,8 @@ ${where.join("\n")}`);
24804
25304
  };
24805
25305
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
24806
25306
  const tracker = new Tracker(route2, cfg.ship);
24807
- const configText = readFileSync36(cfg.configFile, "utf8");
24808
- const cfgBase = dirname22(cfg.configFile);
25307
+ const configText = readFileSync37(cfg.configFile, "utf8");
25308
+ const cfgBase = dirname23(cfg.configFile);
24809
25309
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
24810
25310
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
24811
25311
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -24833,7 +25333,7 @@ ${where.join("\n")}`);
24833
25333
  }
24834
25334
  if (configured2.some((r) => r.dir === dir)) {
24835
25335
  try {
24836
- writeFileSync28(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
25336
+ writeFileSync29(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
24837
25337
  } catch (e) {
24838
25338
  if (e instanceof ReposError) fail(e.message);
24839
25339
  throw e;
@@ -24841,29 +25341,29 @@ ${where.join("\n")}`);
24841
25341
  }
24842
25342
  if (row) {
24843
25343
  await tracker.deleteRepoRow(row.id);
24844
- if (existsSync41(resolvedFile)) {
24845
- const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
25344
+ if (existsSync42(resolvedFile)) {
25345
+ const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
24846
25346
  if (raw.repoNames) delete raw.repoNames[row.id];
24847
25347
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
24848
- writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
25348
+ writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
24849
25349
  `);
24850
25350
  }
24851
25351
  }
24852
- if (existsSync41(join44(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync19(dir, { recursive: true, force: true });
25352
+ if (existsSync42(join46(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync20(dir, { recursive: true, force: true });
24853
25353
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
24854
25354
  break;
24855
25355
  }
24856
25356
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
24857
- const fresh = !existsSync41(join44(dir, ".git"));
24858
- if (existsSync41(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
25357
+ const fresh = !existsSync42(join46(dir, ".git"));
25358
+ if (existsSync42(dir) && fresh && readdirSync19(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
24859
25359
  if (dryRun) {
24860
25360
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
24861
25361
  break;
24862
25362
  }
24863
25363
  if (fresh) {
24864
- mkdirSync26(dir, { recursive: true });
24865
- writeFileSync28(join44(dir, "README.md"), SAMPLE_README);
24866
- writeFileSync28(join44(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
25364
+ mkdirSync27(dir, { recursive: true });
25365
+ writeFileSync29(join46(dir, "README.md"), SAMPLE_README);
25366
+ writeFileSync29(join46(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
24867
25367
  const g = (...a) => {
24868
25368
  const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
24869
25369
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -24881,17 +25381,17 @@ ${where.join("\n")}`);
24881
25381
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
24882
25382
  });
24883
25383
  repoId = (await tracker.createRepoRow(payload)).id;
24884
- if (existsSync41(resolvedFile)) {
24885
- const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
25384
+ if (existsSync42(resolvedFile)) {
25385
+ const raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
24886
25386
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
24887
25387
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
24888
- writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
25388
+ writeFileSync29(resolvedFile, `${JSON.stringify(raw, null, 2)}
24889
25389
  `);
24890
25390
  }
24891
25391
  }
24892
25392
  if (!configured2.some((r) => r.dir === dir)) {
24893
25393
  try {
24894
- writeFileSync28(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
25394
+ writeFileSync29(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
24895
25395
  } catch (e) {
24896
25396
  if (e instanceof ReposError) fail(e.message);
24897
25397
  throw e;
@@ -25124,7 +25624,7 @@ ${where.join("\n")}`);
25124
25624
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
25125
25625
  // config-provenance display; this is the field that actually
25126
25626
  // describes where a worktree lands by default.
25127
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
25627
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
25128
25628
  worktreeParent: resolvePath(r.dir, "..")
25129
25629
  })),
25130
25630
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -25229,7 +25729,7 @@ watermark: ${state.watermark()}
25229
25729
  }
25230
25730
  for (const r of repos) {
25231
25731
  const remote = r.config.branch.remote;
25232
- if (existsSync41(r.dir)) {
25732
+ if (existsSync42(r.dir)) {
25233
25733
  pushAuth.push({
25234
25734
  route: c.route,
25235
25735
  repo: r.name,
@@ -25315,7 +25815,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
25315
25815
  }
25316
25816
  try {
25317
25817
  const evFile = eventFileFor(cfg.ship.stateDir);
25318
- const evSize = existsSync41(evFile) ? statSync14(evFile).size : 0;
25818
+ const evSize = existsSync42(evFile) ? statSync14(evFile).size : 0;
25319
25819
  process.stdout.write(
25320
25820
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
25321
25821
  ` : "events log: ok\n"
@@ -25326,7 +25826,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
25326
25826
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
25327
25827
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
25328
25828
  try {
25329
- return readFileSync36(p, "utf8");
25829
+ return readFileSync37(p, "utf8");
25330
25830
  } catch {
25331
25831
  return null;
25332
25832
  }
@@ -25369,7 +25869,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
25369
25869
  );
25370
25870
  for (const r of await resolvedRepos(route2)) {
25371
25871
  const { config } = r;
25372
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
25872
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
25373
25873
  process.stdout.write(
25374
25874
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
25375
25875
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -25756,6 +26256,11 @@ ${foreign.join("\n")}
25756
26256
  );
25757
26257
  }
25758
26258
  }
26259
+ for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
26260
+ const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
26261
+ process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret})
26262
+ `);
26263
+ }
25759
26264
  if (cfg.ship.relayHost && !flag("offline")) {
25760
26265
  const relayUrl = routeRelayUrl(route2);
25761
26266
  if (!relayUrl) {
@@ -25803,6 +26308,8 @@ var parseTicketNumber2;
25803
26308
  var routeRelayUrl2;
25804
26309
  var passengerApiKeys2;
25805
26310
  var endpointApiFor2;
26311
+ var periscopeEndpointDeps2;
26312
+ var periscopeMcpDeps2;
25806
26313
  var endpointReconcileDeps2;
25807
26314
  var releaseRefused2;
25808
26315
  var doctorMachineId2;