@tablation/crew 0.117.1 → 0.119.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 +600 -57
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2547,7 +2547,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
2547
2547
  "hooks",
2548
2548
  "labels",
2549
2549
  "release",
2550
- "sandbox"
2550
+ "sandbox",
2551
+ "periscope"
2551
2552
  ]);
2552
2553
  var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
2553
2554
  var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
@@ -2668,6 +2669,7 @@ function parseRepoConfig(text2, file) {
2668
2669
  rejectUnknown(raw.release?.isolateFailures, ISOLATE_KEYS, "release.isolateFailures", file);
2669
2670
  rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
2670
2671
  rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
2672
+ rejectUnknown(raw.periscope, /* @__PURE__ */ new Set(["inspector"]), "periscope", file);
2671
2673
  rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
2672
2674
  rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm", "hosts", "toolchains"]), "sandbox", file);
2673
2675
  const sandboxNetwork = parseSandboxNetwork(raw.sandbox, file, (m) => new RepoConfigError(m));
@@ -2851,10 +2853,15 @@ function parseRepoConfig(text2, file) {
2851
2853
  if (versionFiles.length === 0) {
2852
2854
  throw new RepoConfigError(`${file}: release.versionFiles cannot be empty`);
2853
2855
  }
2856
+ const inspectorRaw = raw.periscope?.inspector;
2857
+ if (inspectorRaw !== void 0 && inspectorRaw !== "proxy" && inspectorRaw !== "repo") {
2858
+ throw new RepoConfigError(`${file}: periscope.inspector must be "proxy" or "repo"`);
2859
+ }
2854
2860
  return {
2855
2861
  version: SPEC_VERSION,
2856
2862
  branch,
2857
2863
  worktrees: worktrees2,
2864
+ ...inspectorRaw ? { periscope: { inspector: inspectorRaw } } : {},
2858
2865
  docs,
2859
2866
  ...sandbox ? { sandbox } : {},
2860
2867
  platform: platform2,
@@ -6932,6 +6939,69 @@ var PERISCOPE_COLUMNS = [
6932
6939
  "expires_at"
6933
6940
  ];
6934
6941
 
6942
+ // src/tool-classify.ts
6943
+ var INTEL_SERVER_NAME2 = "code";
6944
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
6945
+ "Grep",
6946
+ "Glob",
6947
+ "LS",
6948
+ // Claude
6949
+ "search_file_content",
6950
+ "glob",
6951
+ "list_directory",
6952
+ // Gemini
6953
+ "grep_search",
6954
+ "file_search"
6955
+ ]);
6956
+ var EDIT_TOOLS = /* @__PURE__ */ new Set([
6957
+ "Edit",
6958
+ "Write",
6959
+ "MultiEdit",
6960
+ "NotebookEdit",
6961
+ // Claude
6962
+ "file_change",
6963
+ // Codex
6964
+ "replace",
6965
+ "write_file"
6966
+ // Gemini
6967
+ ]);
6968
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "command", "run_shell_command"]);
6969
+ var SEARCH_COMMANDS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "find", "ls", "fd", "ag"]);
6970
+ function classifyTool(tool, target) {
6971
+ if (!tool) return "other";
6972
+ if (tool.startsWith(`mcp__${INTEL_SERVER_NAME2}__`)) return "symbol";
6973
+ if (EDIT_TOOLS.has(tool)) return "edit";
6974
+ if (SEARCH_TOOLS.has(tool)) return "search";
6975
+ if (SHELL_TOOLS.has(tool)) {
6976
+ const first = (target ?? "").split("/").pop();
6977
+ return first && SEARCH_COMMANDS.has(first) ? "search" : "other";
6978
+ }
6979
+ return "other";
6980
+ }
6981
+ var BeforeFirstEditTracker = class {
6982
+ calls = { search: 0, symbol: 0, other: 0 };
6983
+ edited = false;
6984
+ seconds;
6985
+ /** `elapsedMs` is the time since session start, when known (not on a replayed stream). */
6986
+ note(tool, target, elapsedMs) {
6987
+ if (this.edited) return;
6988
+ const kind = classifyTool(tool, target);
6989
+ if (kind === "edit") {
6990
+ this.edited = true;
6991
+ if (elapsedMs !== void 0) this.seconds = Math.max(0, Math.round(elapsedMs / 1e3));
6992
+ } else {
6993
+ this.calls[kind]++;
6994
+ }
6995
+ }
6996
+ result() {
6997
+ return {
6998
+ calls: { ...this.calls },
6999
+ edited: this.edited,
7000
+ ...this.seconds !== void 0 ? { secondsToFirstEdit: this.seconds } : {}
7001
+ };
7002
+ }
7003
+ };
7004
+
6935
7005
  // src/agent-log.ts
6936
7006
  import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "fs";
6937
7007
  import { join as join18 } from "path";
@@ -7006,6 +7076,36 @@ function counterIncrements(run3, cols, have) {
7006
7076
  }
7007
7077
  return out;
7008
7078
  }
7079
+ var BEFORE_EDIT_COLUMNS = {
7080
+ search: "calls_before_edit_search",
7081
+ symbol: "calls_before_edit_symbol",
7082
+ other: "calls_before_edit_other",
7083
+ seconds: "seconds_to_first_edit",
7084
+ edited: "edited"
7085
+ };
7086
+ function beforeFirstEditBody(b) {
7087
+ return {
7088
+ [BEFORE_EDIT_COLUMNS.search]: b.calls.search,
7089
+ [BEFORE_EDIT_COLUMNS.symbol]: b.calls.symbol,
7090
+ [BEFORE_EDIT_COLUMNS.other]: b.calls.other,
7091
+ [BEFORE_EDIT_COLUMNS.edited]: b.edited,
7092
+ ...b.secondsToFirstEdit !== void 0 ? { [BEFORE_EDIT_COLUMNS.seconds]: b.secondsToFirstEdit } : {}
7093
+ };
7094
+ }
7095
+ async function writeBeforeFirstEdit(target, rowId, b) {
7096
+ try {
7097
+ const model = await request(
7098
+ target,
7099
+ "GET",
7100
+ `/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
7101
+ );
7102
+ const have = await columnsOf(target, model.id);
7103
+ const body = Object.fromEntries(Object.entries(beforeFirstEditBody(b)).filter(([column]) => have.has(column)));
7104
+ if (Object.keys(body).length === 0) return;
7105
+ await request(target, "PATCH", `/data-models/${model.id}/records/${rowId}`, body);
7106
+ } catch {
7107
+ }
7108
+ }
7009
7109
  async function post(target, path, body) {
7010
7110
  const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
7011
7111
  const res = await fetch(url, {
@@ -7074,6 +7174,7 @@ async function reportAgentRun(target, run3) {
7074
7174
  ...cycle.role ? { role: cycle.role } : {}
7075
7175
  });
7076
7176
  }
7177
+ if (run3.beforeFirstEdit) await writeBeforeFirstEdit(target, entry.id, run3.beforeFirstEdit);
7077
7178
  const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
7078
7179
  return {
7079
7180
  agentLogId: entry.id,
@@ -8604,12 +8705,14 @@ function listeners() {
8604
8705
  return out;
8605
8706
  }
8606
8707
  var pidsOnPort = (port) => listeners().filter((l) => l.port === port).map((l) => l.pid);
8607
- var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, span: 1e3 };
8708
+ var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, proxyBase: 5e4, span: 1e3 };
8608
8709
  function ticketForPort(port, s = DEFAULT_PORTS) {
8609
8710
  if (port >= s.backendBase && port < s.backendBase + s.span) return { n: port - s.backendBase, role: "backend" };
8610
8711
  if (port >= s.frontendBase && port < s.frontendBase + s.span) return { n: port - s.frontendBase, role: "frontend" };
8712
+ if (s.proxyBase !== void 0 && port >= s.proxyBase && port < s.proxyBase + s.span) return { n: port - s.proxyBase, role: "proxy" };
8611
8713
  return null;
8612
8714
  }
8715
+ var proxyPortFor = (n, s = DEFAULT_PORTS) => (s.proxyBase ?? DEFAULT_PORTS.proxyBase) + n;
8613
8716
  function worktreeExists(parent, prefix, n) {
8614
8717
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8615
8718
  if (existsSync18(join19(parent, name, ".git"))) return true;
@@ -8645,7 +8748,7 @@ async function killGently(pids, graceMs = 2e3) {
8645
8748
  }
8646
8749
  function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
8647
8750
  const pids = /* @__PURE__ */ new Set();
8648
- for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
8751
+ for (const port of [s.backendBase + n, s.frontendBase + n, ...s.proxyBase !== void 0 ? [s.proxyBase + n] : []]) for (const p of pidsOnPort(port)) pids.add(p);
8649
8752
  const matched = run("pgrep", ["-f", `${path}/`]);
8650
8753
  for (const line of matched.split("\n")) {
8651
8754
  const pid = Number(line.trim());
@@ -10588,6 +10691,8 @@ var RunObserver = class {
10588
10691
  result;
10589
10692
  /** Prefix tokens of the first assistant call that carried usage; kept once. */
10590
10693
  prefixTokens;
10694
+ /** Tool calls before the run's first edit (CREW-2879). */
10695
+ beforeFirstEdit = new BeforeFirstEditTracker();
10591
10696
  /** Set once the stream carries the harness's final result (turn count or cost). */
10592
10697
  sawResult = false;
10593
10698
  muted = false;
@@ -10599,6 +10704,7 @@ var RunObserver = class {
10599
10704
  /** Called after `liveTicket` changes, so the sidecar can be rewritten. */
10600
10705
  onClaim;
10601
10706
  buf = "";
10707
+ startMs = Date.now();
10602
10708
  mapper;
10603
10709
  id;
10604
10710
  adapter;
@@ -10654,6 +10760,7 @@ var RunObserver = class {
10654
10760
  for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
10655
10761
  for (const ev of events) {
10656
10762
  if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
10763
+ if (ev.kind === "tool") this.beforeFirstEdit.note(ev.tool, ev.target, this.muted ? void 0 : Date.now() - this.startMs);
10657
10764
  if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
10658
10765
  const claimed = this.attribution.current;
10659
10766
  if (claimed && claimed !== this.liveTicket) {
@@ -10768,6 +10875,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10768
10875
  emit.warn(`could not save the pending hand-off: ${e.message}`, { step: "agent" });
10769
10876
  }
10770
10877
  }
10878
+ const before = obs.beforeFirstEdit.result();
10771
10879
  try {
10772
10880
  writeRunMeta(id, startedAt, obs.liveTicket, {
10773
10881
  attributedTicket: attributedTicket ?? null,
@@ -10779,6 +10887,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10779
10887
  sessionId: result?.sessionId ?? null,
10780
10888
  numTurns: result?.numTurns ?? null,
10781
10889
  prefixTokens: obs.prefixTokens ?? null,
10890
+ beforeFirstEdit: before,
10782
10891
  totalCostUsd: result?.costUsd ?? null
10783
10892
  });
10784
10893
  } catch {
@@ -10794,6 +10903,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10794
10903
  attributedTicket: attributedTicket ?? null,
10795
10904
  attributedBy,
10796
10905
  ...result?.sessionId ? { sessionId: result.sessionId } : {},
10906
+ callsBeforeFirstEdit: before.calls,
10907
+ edited: before.edited,
10908
+ ...before.secondsToFirstEdit !== void 0 ? { secondsToFirstEdit: before.secondsToFirstEdit } : {},
10797
10909
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
10798
10910
  ...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
10799
10911
  }
@@ -10814,6 +10926,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10814
10926
  // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
10815
10927
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
10816
10928
  ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
10929
+ beforeFirstEdit: before,
10817
10930
  harness: identity.harness,
10818
10931
  provider: identity.provider,
10819
10932
  promptVersion: id.promptVersion,
@@ -12669,7 +12782,7 @@ function doctorSelftestLines(stored, toolchains) {
12669
12782
  }
12670
12783
 
12671
12784
  // src/cli.ts
12672
- import { resolve as resolve7, dirname as dirname30, join as join51, basename as basename16 } from "path";
12785
+ import { resolve as resolve7, dirname as dirname31, join as join51, basename as basename16 } from "path";
12673
12786
  import { fileURLToPath } from "url";
12674
12787
  import { hostname as hostname4, homedir as homedir11 } from "os";
12675
12788
  import { spawnSync as spawnSync10 } from "child_process";
@@ -19591,7 +19704,20 @@ async function listLogEntries(route2, opts = {}) {
19591
19704
  limit: opts.limit ?? 20,
19592
19705
  ...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
19593
19706
  });
19594
- return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
19707
+ const newestFirst = [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
19708
+ if (!opts.beforeEdit) return newestFirst;
19709
+ const searches = (e) => e.calls_before_edit_search ?? -1;
19710
+ return newestFirst.sort((a, b) => searches(b) - searches(a));
19711
+ }
19712
+ function formatBeforeEdit(e) {
19713
+ if (e.calls_before_edit_search == null && e.calls_before_edit_symbol == null && e.calls_before_edit_other == null) return void 0;
19714
+ const parts = [
19715
+ `search ${e.calls_before_edit_search ?? 0}`,
19716
+ `symbol ${e.calls_before_edit_symbol ?? 0}`,
19717
+ `other ${e.calls_before_edit_other ?? 0}`
19718
+ ];
19719
+ const when = e.edited === false ? "no edit" : e.seconds_to_first_edit != null ? `${e.seconds_to_first_edit}s to first edit` : void 0;
19720
+ return `${parts.join(", ")}${when ? ` (${when})` : ""}`;
19595
19721
  }
19596
19722
  async function showLogEntry(route2, entryId, opts = {}) {
19597
19723
  requireWorkspace(route2);
@@ -20307,7 +20433,7 @@ function warnOrphanedWorktreeDirs(paths, log, held) {
20307
20433
  }
20308
20434
  async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base, held) {
20309
20435
  const refused = [];
20310
- const refuse = (a, reason, detail) => {
20436
+ const refuse2 = (a, reason, detail) => {
20311
20437
  refused.push(a.path);
20312
20438
  const when = held ? noteHeldWorktree(held.state, { repo: held.repo, path: a.path, ticket: a.ticket.issue_id, status: a.ticket.status, reason }) : "first";
20313
20439
  if (when) {
@@ -20335,12 +20461,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
20335
20461
  } catch {
20336
20462
  }
20337
20463
  if (dirty) {
20338
- refuse(a, "uncommitted changes", "it has uncommitted changes");
20464
+ refuse2(a, "uncommitted changes", "it has uncommitted changes");
20339
20465
  continue;
20340
20466
  }
20341
20467
  }
20342
20468
  if (!dryRun && a.requirePushed && a.branch && existsSync39(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
20343
- refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
20469
+ refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
20344
20470
  continue;
20345
20471
  }
20346
20472
  const n = Number(a.ticket.issue_id.replace(/^\D+/, ""));
@@ -20364,7 +20490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
20364
20490
  try {
20365
20491
  git(cwd, ["worktree", "remove", "--force", a.path]);
20366
20492
  } catch (e) {
20367
- refuse(a, "removal failed", `could not remove it: ${e.message}`);
20493
+ refuse2(a, "removal failed", `could not remove it: ${e.message}`);
20368
20494
  continue;
20369
20495
  }
20370
20496
  if (!a.keepBranch && a.branch) {
@@ -20538,7 +20664,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
20538
20664
  }
20539
20665
 
20540
20666
  // src/cli.ts
20541
- import { realpathSync as realpathSync3, readFileSync as readFileSync42, existsSync as existsSync47, mkdirSync as mkdirSync34, writeFileSync as writeFileSync35, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
20667
+ import { realpathSync as realpathSync3, readFileSync as readFileSync42, existsSync as existsSync47, mkdirSync as mkdirSync35, writeFileSync as writeFileSync35, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
20542
20668
  import { createInterface } from "readline/promises";
20543
20669
 
20544
20670
  // src/periscope-cmd.ts
@@ -20744,9 +20870,18 @@ ${tail(handoff.output)}`);
20744
20870
  return abandon(`preview exited ${preview?.code ?? "without running"}
20745
20871
  ${tail(preview?.output ?? "")}`);
20746
20872
  }
20747
- const url = previewUrlFrom(preview.output);
20748
- if (!url) return abandon(`preview printed no URL as its last line
20873
+ const previewUrl = previewUrlFrom(preview.output);
20874
+ if (!previewUrl) return abandon(`preview printed no URL as its last line
20749
20875
  ${tail(preview.output)}`);
20876
+ let url = previewUrl;
20877
+ if (d.startProxy) {
20878
+ try {
20879
+ url = await d.startProxy(wt.path, previewUrl, /(\d+)$/.exec(draft.issueId)?.[1] ?? "0");
20880
+ } catch (e) {
20881
+ return abandon(`starting the periscope proxy failed: ${e.message}`);
20882
+ }
20883
+ d.say("proxy", `proxy at ${url} in front of ${previewUrl}`, { url, previewUrl });
20884
+ }
20750
20885
  d.say("preview", `preview at ${url}`, { url });
20751
20886
  const intel = await probeIntel(
20752
20887
  { runHook: (e) => d.runHook("intel", wt.path, e) },
@@ -20935,10 +21070,17 @@ ${section}`;
20935
21070
  forgetLocal(d, row.ticketRecordId);
20936
21071
  return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha, ...existing ? { existing: true, ...prior ? { status: prior.status } : {} } : {} };
20937
21072
  }
21073
+ function isProxyUrl(url, number) {
21074
+ try {
21075
+ return Number(new URL(url).port) === proxyPortFor(Number(number));
21076
+ } catch {
21077
+ return false;
21078
+ }
21079
+ }
20938
21080
  function periscopeListEntries(rows, now, isListening) {
20939
21081
  return rows.filter((r) => r.state === "live").map((r) => {
20940
21082
  const at = r.lastActivityAt ? new Date(r.lastActivityAt).getTime() : NaN;
20941
- return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r) };
21083
+ return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r), proxied: isProxyUrl(r.url, r.number) };
20942
21084
  });
20943
21085
  }
20944
21086
  function formatPeriscopeList(entries) {
@@ -20947,7 +21089,7 @@ function formatPeriscopeList(entries) {
20947
21089
  e.ticket,
20948
21090
  e.repo,
20949
21091
  e.worktree,
20950
- e.url,
21092
+ e.proxied ? `${e.url} (proxy)` : e.url,
20951
21093
  e.state,
20952
21094
  e.idleMinutes === null ? "idle ?" : `idle ${e.idleMinutes} min`,
20953
21095
  e.listening ? "listening" : "not listening"
@@ -20979,9 +21121,16 @@ ${tail2(iso.output)}`);
20979
21121
  throw new PeriscopeError(`preview exited ${preview?.code ?? "without running"}
20980
21122
  ${tail2(preview?.output ?? "")}`);
20981
21123
  }
20982
- const url = previewUrlFrom(preview.output);
21124
+ let url = previewUrlFrom(preview.output);
20983
21125
  if (!url) throw new PeriscopeError(`preview printed no URL as its last line
20984
21126
  ${tail2(preview.output)}`);
21127
+ if (d.startProxy) {
21128
+ try {
21129
+ url = await d.startProxy(row.worktree, url, /(\d+)$/.exec(tag)?.[1] ?? "0");
21130
+ } catch (e) {
21131
+ throw new PeriscopeError(`starting the periscope proxy failed: ${e.message}`);
21132
+ }
21133
+ }
20985
21134
  const now = d.now();
20986
21135
  await d.update(row.id, {
20987
21136
  url,
@@ -21517,6 +21666,388 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
21517
21666
  return endpoints;
21518
21667
  }
21519
21668
 
21669
+ // src/periscope-proxy.ts
21670
+ import { spawn as spawn9 } from "child_process";
21671
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync32 } from "fs";
21672
+ import { createServer, request as httpRequest } from "http";
21673
+ import { request as httpsRequest } from "https";
21674
+ import { connect as netConnect } from "net";
21675
+ import { connect as tlsConnect } from "tls";
21676
+ import { dirname as dirname28, isAbsolute as isAbsolute4 } from "path";
21677
+ import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
21678
+
21679
+ // src/periscope-inspector.ts
21680
+ var INSPECTOR_ROUTE = "/__periscope/inspector.js";
21681
+ var POINTED_ROUTE = "/__periscope/pointed";
21682
+ var INSPECTOR_SOURCE = `
21683
+ const ROUTE = ${JSON.stringify(POINTED_ROUTE)};
21684
+ let armed = false;
21685
+ let off = false;
21686
+ let current = null;
21687
+ let toast = null;
21688
+ const outline = document.createElement('div');
21689
+ const label = document.createElement('div');
21690
+ Object.assign(outline.style, { position: 'fixed', zIndex: '2147483646', pointerEvents: 'none', outline: '2px solid #e11d8f', outlineOffset: '-1px', display: 'none' });
21691
+ Object.assign(label.style, { position: 'fixed', zIndex: '2147483647', pointerEvents: 'none', font: '11px/1.3 ui-monospace, monospace', background: '#1f1f23', color: '#fff', padding: '2px 6px', borderRadius: '3px', display: 'none', maxWidth: '60vw', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' });
21692
+ document.documentElement.append(outline, label);
21693
+
21694
+ function componentName(el) {
21695
+ const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
21696
+ let fiber = key ? el[key] : null;
21697
+ while (fiber) {
21698
+ const t = fiber.type;
21699
+ if (typeof t === 'function' || (t && typeof t === 'object')) {
21700
+ const n = (typeof t === 'function' ? t.displayName || t.name : t.displayName || (t.render && (t.render.displayName || t.render.name)) || (t.type && (t.type.displayName || t.type.name)));
21701
+ if (n) return n;
21702
+ }
21703
+ fiber = fiber.return;
21704
+ }
21705
+ return undefined;
21706
+ }
21707
+
21708
+ function hide() {
21709
+ current = null;
21710
+ outline.style.display = 'none';
21711
+ label.style.display = 'none';
21712
+ }
21713
+
21714
+ function sync() {
21715
+ if (!armed || off) { hide(); document.documentElement.style.cursor = ''; return; }
21716
+ document.documentElement.style.cursor = 'crosshair';
21717
+ }
21718
+
21719
+ function show(el) {
21720
+ current = el;
21721
+ const r = el.getBoundingClientRect();
21722
+ Object.assign(outline.style, { display: 'block', left: r.left + 'px', top: r.top + 'px', width: r.width + 'px', height: r.height + 'px' });
21723
+ label.textContent = el.getAttribute('data-source') + (componentName(el) ? ' <' + componentName(el) + '>' : '');
21724
+ label.style.display = 'block';
21725
+ label.style.left = Math.max(0, r.left) + 'px';
21726
+ label.style.top = (r.top > 20 ? r.top - 20 : r.bottom + 2) + 'px';
21727
+ }
21728
+
21729
+ function flash(text) {
21730
+ if (toast) toast.remove();
21731
+ toast = document.createElement('div');
21732
+ toast.textContent = text;
21733
+ Object.assign(toast.style, { position: 'fixed', zIndex: '2147483647', right: '12px', bottom: '12px', font: '12px ui-monospace, monospace', background: '#1f1f23', color: '#fff', padding: '6px 10px', borderRadius: '4px', pointerEvents: 'none' });
21734
+ document.documentElement.append(toast);
21735
+ setTimeout(() => { if (toast) { toast.remove(); toast = null; } }, 1500);
21736
+ }
21737
+
21738
+ window.addEventListener('keydown', (e) => {
21739
+ if (e.key === 'Alt') { armed = true; off = false; sync(); }
21740
+ else if (e.key === 'Escape' && armed) { off = true; sync(); }
21741
+ }, true);
21742
+ window.addEventListener('keyup', (e) => { if (e.key === 'Alt') { armed = false; off = false; sync(); } }, true);
21743
+ window.addEventListener('blur', () => { armed = false; off = false; sync(); });
21744
+
21745
+ window.addEventListener('mousemove', (e) => {
21746
+ if (!armed || off) return;
21747
+ const target = e.target instanceof Element ? e.target.closest('[data-source]') : null;
21748
+ if (target) show(target); else hide();
21749
+ }, true);
21750
+
21751
+ window.addEventListener('click', (e) => {
21752
+ if (!armed || off || !current) return;
21753
+ e.preventDefault();
21754
+ e.stopPropagation();
21755
+ const src = current.getAttribute('data-source');
21756
+ const i = src.lastIndexOf(':');
21757
+ const entry = { path: src.slice(0, i), line: Number(src.slice(i + 1)), component: componentName(current), url: location.href };
21758
+ fetch(ROUTE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) })
21759
+ .then((r) => { if (!r.ok) throw new Error(String(r.status)); })
21760
+ .catch(() => flash('point failed'));
21761
+ if (navigator.clipboard) navigator.clipboard.writeText(src).catch(() => {});
21762
+ flash('pointed at ' + src);
21763
+ }, true);
21764
+ `;
21765
+
21766
+ // src/periscope-proxy.ts
21767
+ var MAX_BODY_BYTES = 8 * 1024;
21768
+ var SCRIPT_TAG = `<script type="module" src="${INSPECTOR_ROUTE}"></script>`;
21769
+ function parsePointedBody(raw) {
21770
+ let body;
21771
+ try {
21772
+ body = JSON.parse(raw);
21773
+ } catch {
21774
+ return null;
21775
+ }
21776
+ if (!body || typeof body !== "object") return null;
21777
+ const b = body;
21778
+ const p = b.path;
21779
+ const line = b.line;
21780
+ if (typeof p !== "string" || p.length === 0 || p.length > 500) return null;
21781
+ if (isAbsolute4(p) || p.split(/[\\/]/).includes("..")) return null;
21782
+ if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return null;
21783
+ const entry = { path: p, line };
21784
+ if (typeof b.column === "number" && Number.isInteger(b.column) && b.column >= 0) entry.column = b.column;
21785
+ if (typeof b.component === "string" && b.component.length <= 200) entry.component = b.component;
21786
+ if (typeof b.url === "string") entry.url = b.url.slice(0, 2e3);
21787
+ return entry;
21788
+ }
21789
+ function appendPointed(file, entry, now = /* @__PURE__ */ new Date()) {
21790
+ mkdirSync32(dirname28(file), { recursive: true });
21791
+ appendFileSync3(file, `${JSON.stringify({ ...entry, at: now.toISOString() })}
21792
+ `);
21793
+ }
21794
+ var CSP_DIRECTIVES = /* @__PURE__ */ new Set(["default-src", "script-src", "script-src-elem", "connect-src"]);
21795
+ function relaxCsp(policy) {
21796
+ return policy.split(";").map((part) => {
21797
+ const tokens = part.trim().split(/\s+/).filter(Boolean);
21798
+ const name = tokens[0]?.toLowerCase();
21799
+ if (!name || !CSP_DIRECTIVES.has(name)) return part.trim();
21800
+ const sources = tokens.slice(1).filter((t) => t.toLowerCase() !== "'none'");
21801
+ if (!sources.some((t) => t.toLowerCase() === "'self'")) sources.push("'self'");
21802
+ return [tokens[0], ...sources].join(" ");
21803
+ }).filter(Boolean).join("; ");
21804
+ }
21805
+ function injectScript(html) {
21806
+ const i = html.search(/<\/body\s*>(?![\s\S]*<\/body\s*>)/i);
21807
+ return i === -1 ? html + SCRIPT_TAG : html.slice(0, i) + SCRIPT_TAG + html.slice(i);
21808
+ }
21809
+ function decode(body, encoding) {
21810
+ switch ((encoding ?? "").toLowerCase().trim()) {
21811
+ case "":
21812
+ case "identity":
21813
+ return body;
21814
+ case "gzip":
21815
+ case "x-gzip":
21816
+ return gunzipSync(body);
21817
+ case "deflate":
21818
+ return inflateSync(body);
21819
+ case "br":
21820
+ return brotliDecompressSync(body);
21821
+ default:
21822
+ return void 0;
21823
+ }
21824
+ }
21825
+ function forwardHeaders(req, target) {
21826
+ const h = { ...req.headers, host: target.host };
21827
+ if (typeof h.origin === "string") h.origin = target.origin;
21828
+ if (typeof h.referer === "string") {
21829
+ try {
21830
+ const r = new URL(h.referer);
21831
+ h.referer = `${target.origin}${r.pathname}${r.search}`;
21832
+ } catch {
21833
+ }
21834
+ }
21835
+ return h;
21836
+ }
21837
+ function refuse(res, status2, headers = {}) {
21838
+ res.writeHead(status2, headers);
21839
+ res.end();
21840
+ }
21841
+ function handlePointed(req, res, o) {
21842
+ if (req.method !== "POST") return refuse(res, 405, { Allow: "POST" });
21843
+ const chunks = [];
21844
+ let size = 0;
21845
+ let aborted = false;
21846
+ req.on("data", (c) => {
21847
+ size += c.length;
21848
+ if (size > MAX_BODY_BYTES) {
21849
+ aborted = true;
21850
+ refuse(res, 413);
21851
+ req.destroy();
21852
+ return;
21853
+ }
21854
+ chunks.push(c);
21855
+ });
21856
+ req.on("end", () => {
21857
+ if (aborted) return;
21858
+ const entry = parsePointedBody(Buffer.concat(chunks).toString("utf8"));
21859
+ if (!entry) return refuse(res, 400);
21860
+ try {
21861
+ appendPointed(o.dropFile, entry, o.now?.());
21862
+ refuse(res, 204);
21863
+ } catch {
21864
+ refuse(res, 500);
21865
+ }
21866
+ });
21867
+ }
21868
+ function proxyRequest(req, res, o) {
21869
+ const { target } = o;
21870
+ const secure = target.protocol === "https:";
21871
+ const upstream = (secure ? httpsRequest : httpRequest)({
21872
+ hostname: target.hostname,
21873
+ port: target.port || (secure ? 443 : 80),
21874
+ method: req.method,
21875
+ path: req.url,
21876
+ headers: forwardHeaders(req, target)
21877
+ }, (up) => {
21878
+ const headers = { ...up.headers };
21879
+ if (typeof headers.location === "string" && headers.location.startsWith(target.origin)) {
21880
+ headers.location = headers.location.slice(target.origin.length) || "/";
21881
+ }
21882
+ const csp = headers["content-security-policy"];
21883
+ if (o.inject && typeof csp === "string") headers["content-security-policy"] = relaxCsp(csp);
21884
+ const isHtml = /^text\/html\b/i.test(String(up.headers["content-type"] ?? ""));
21885
+ if (!o.inject || !isHtml || req.method === "HEAD") {
21886
+ res.writeHead(up.statusCode ?? 502, headers);
21887
+ up.pipe(res);
21888
+ return;
21889
+ }
21890
+ const chunks = [];
21891
+ up.on("data", (c) => chunks.push(c));
21892
+ up.on("end", () => {
21893
+ let html;
21894
+ try {
21895
+ html = decode(Buffer.concat(chunks), String(up.headers["content-encoding"] ?? ""));
21896
+ } catch {
21897
+ html = void 0;
21898
+ }
21899
+ if (!html) {
21900
+ res.writeHead(up.statusCode ?? 502, headers);
21901
+ res.end(Buffer.concat(chunks));
21902
+ return;
21903
+ }
21904
+ const out = Buffer.from(injectScript(html.toString("utf8")), "utf8");
21905
+ delete headers["content-encoding"];
21906
+ delete headers.etag;
21907
+ delete headers["transfer-encoding"];
21908
+ headers["content-length"] = String(out.length);
21909
+ res.writeHead(up.statusCode ?? 502, headers);
21910
+ res.end(out);
21911
+ });
21912
+ up.on("error", () => res.destroy());
21913
+ });
21914
+ upstream.on("error", (e) => {
21915
+ if (res.headersSent) {
21916
+ res.destroy();
21917
+ return;
21918
+ }
21919
+ res.writeHead(502, { "Content-Type": "text/plain; charset=utf-8" });
21920
+ res.end(`periscope proxy: ${target.origin} is not answering (${e.message})
21921
+ `);
21922
+ });
21923
+ req.pipe(upstream);
21924
+ }
21925
+ function proxyUpgrade(req, client2, head, o) {
21926
+ const { target } = o;
21927
+ const secure = target.protocol === "https:";
21928
+ const port = Number(target.port) || (secure ? 443 : 80);
21929
+ const up = secure ? tlsConnect({ host: target.hostname, port, servername: target.hostname }) : netConnect({ host: target.hostname, port });
21930
+ up.on("error", () => client2.destroy());
21931
+ client2.on("error", () => up.destroy());
21932
+ const hangUp = () => {
21933
+ up.destroy();
21934
+ client2.destroy();
21935
+ };
21936
+ client2.on("end", hangUp);
21937
+ client2.on("close", hangUp);
21938
+ up.on("end", hangUp);
21939
+ up.on("close", hangUp);
21940
+ up.on(secure ? "secureConnect" : "connect", () => {
21941
+ const headers = forwardHeaders(req, target);
21942
+ const lines2 = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
21943
+ for (const [k, v] of Object.entries(headers)) {
21944
+ if (v === void 0) continue;
21945
+ for (const one of Array.isArray(v) ? v : [v]) lines2.push(`${k}: ${one}`);
21946
+ }
21947
+ up.write(`${lines2.join("\r\n")}\r
21948
+ \r
21949
+ `);
21950
+ if (head.length) up.write(head);
21951
+ up.pipe(client2);
21952
+ client2.pipe(up);
21953
+ });
21954
+ }
21955
+ function createPeriscopeProxy(o) {
21956
+ const server = createServer(async (req, res) => {
21957
+ try {
21958
+ if (o.requestFilter && !await o.requestFilter(req, res)) return;
21959
+ const path = (req.url ?? "/").split("?")[0];
21960
+ if (o.inject && path === INSPECTOR_ROUTE && (req.method === "GET" || req.method === "HEAD")) {
21961
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-store" });
21962
+ res.end(req.method === "HEAD" ? void 0 : INSPECTOR_SOURCE);
21963
+ return;
21964
+ }
21965
+ if (o.inject && path === POINTED_ROUTE) return handlePointed(req, res, o);
21966
+ proxyRequest(req, res, o);
21967
+ } catch {
21968
+ if (!res.headersSent) refuse(res, 500);
21969
+ else res.destroy();
21970
+ }
21971
+ });
21972
+ server.on("upgrade", async (req, socket, head) => {
21973
+ try {
21974
+ if (o.requestFilter && !await o.requestFilter(req, socket)) return;
21975
+ proxyUpgrade(req, socket, head, o);
21976
+ } catch {
21977
+ socket.destroy();
21978
+ }
21979
+ });
21980
+ return server;
21981
+ }
21982
+ function proxyEnv(l) {
21983
+ return {
21984
+ CREW_PROXY_TARGET: l.target,
21985
+ CREW_PROXY_HOST: l.host,
21986
+ CREW_PROXY_PORT: String(l.port),
21987
+ CREW_PROXY_INJECT: l.inject ? "1" : "0"
21988
+ };
21989
+ }
21990
+ function proxiedUrl(previewUrl, host, port) {
21991
+ const u = new URL(previewUrl);
21992
+ return `http://${host.includes(":") ? `[${host}]` : host}:${port}${u.pathname === "/" && !u.search ? "/" : u.pathname}${u.search}`;
21993
+ }
21994
+ function runProxyFromEnv(dropFile, env = process.env) {
21995
+ const target = env.CREW_PROXY_TARGET;
21996
+ const port = Number(env.CREW_PROXY_PORT);
21997
+ if (!target || !Number.isInteger(port) || port <= 0) throw new Error("periscope-proxy: CREW_PROXY_TARGET and CREW_PROXY_PORT are required");
21998
+ const server = createPeriscopeProxy({ target: new URL(target), dropFile, inject: env.CREW_PROXY_INJECT !== "0" });
21999
+ server.on("error", (e) => {
22000
+ process.stderr.write(`periscope-proxy: ${e.message}
22001
+ `);
22002
+ process.exit(1);
22003
+ });
22004
+ server.listen(port, env.CREW_PROXY_HOST || "localhost");
22005
+ return server;
22006
+ }
22007
+ function accepts(host, port, timeoutMs = 500) {
22008
+ return new Promise((resolve8) => {
22009
+ const s = netConnect({ host, port });
22010
+ const done = (ok) => {
22011
+ s.destroy();
22012
+ resolve8(ok);
22013
+ };
22014
+ s.setTimeout(timeoutMs, () => done(false));
22015
+ s.once("connect", () => done(true));
22016
+ s.once("error", () => done(false));
22017
+ });
22018
+ }
22019
+ async function launchProxy(invocation, worktree, previewUrl, number, o) {
22020
+ const port = proxyPortFor(number);
22021
+ const stale = pidsOnPort(port);
22022
+ if (stale.length) await killGently(stale);
22023
+ const launch = { worktree, target: previewUrl, host: o.host, port, inject: o.inject };
22024
+ const [cmd, ...args] = invocation;
22025
+ const child = spawn9(cmd, [...args, "periscope-proxy", pointedAtFile(worktree)], {
22026
+ detached: true,
22027
+ stdio: "ignore",
22028
+ cwd: worktree,
22029
+ env: { ...process.env, ...proxyEnv(launch) }
22030
+ });
22031
+ child.unref();
22032
+ let exited = false;
22033
+ child.on("exit", () => {
22034
+ exited = true;
22035
+ });
22036
+ const deadline = Date.now() + (o.waitMs ?? 1e4);
22037
+ while (Date.now() < deadline) {
22038
+ if (exited) throw new Error(`the proxy process exited before listening on ${o.host}:${port}`);
22039
+ if (await accepts(o.host, port)) return proxiedUrl(previewUrl, o.host, port);
22040
+ await new Promise((r) => setTimeout(r, 100));
22041
+ }
22042
+ if (child.pid) {
22043
+ try {
22044
+ process.kill(child.pid, "SIGTERM");
22045
+ } catch {
22046
+ }
22047
+ }
22048
+ throw new Error(`the proxy did not start listening on ${o.host}:${port}`);
22049
+ }
22050
+
21520
22051
  // src/outage.ts
21521
22052
  var TRANSIENT_BACKOFF_MS = [15e3, 3e4, 6e4];
21522
22053
  var OUTAGE_ESCALATE_AFTER = 5;
@@ -21549,7 +22080,7 @@ function describeCause(err3) {
21549
22080
  }
21550
22081
 
21551
22082
  // src/daemon.ts
21552
- import { existsSync as existsSync44, mkdirSync as mkdirSync32, readFileSync as readFileSync40, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync14, writeFileSync as writeFileSync33 } from "fs";
22083
+ import { existsSync as existsSync44, mkdirSync as mkdirSync33, readFileSync as readFileSync40, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync14, writeFileSync as writeFileSync33 } from "fs";
21553
22084
  import { join as join47 } from "path";
21554
22085
  import { randomBytes as randomBytes2 } from "crypto";
21555
22086
  var DEFAULT_FLOOR_MS = 5e3;
@@ -21663,7 +22194,7 @@ function daemonStampFor(stateDir) {
21663
22194
  return join47(stateDir, "daemon.started");
21664
22195
  }
21665
22196
  function noteDaemonStarted(stateDir, now = Date.now()) {
21666
- mkdirSync32(stateDir, { recursive: true });
22197
+ mkdirSync33(stateDir, { recursive: true });
21667
22198
  rmSync20(reloadSentinelFor(stateDir), { force: true });
21668
22199
  writeFileSync33(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
21669
22200
  `);
@@ -21735,7 +22266,7 @@ async function runOnePassUnguarded(o) {
21735
22266
  await hydrateApiKeys([o.route], o.getSessionStore);
21736
22267
  const emit = o.newEmitter();
21737
22268
  const decide = o.decide ?? decideCycle;
21738
- const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
22269
+ const spawn12 = o.spawnRoleAgent ?? runRoleAgent;
21739
22270
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
21740
22271
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
21741
22272
  if (o.sandboxCache) {
@@ -21846,7 +22377,7 @@ async function runOnePassUnguarded(o) {
21846
22377
  settle = resolve8;
21847
22378
  });
21848
22379
  const ready = (tookTicket2) => settle(tookTicket2);
21849
- const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
22380
+ const run3 = spawn12(role, decision, ctx, { ready }).then(() => {
21850
22381
  breaker.recordSuccess(role);
21851
22382
  clearShipAttention(o.state, `role_parked:${role}`);
21852
22383
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -22246,12 +22777,12 @@ import {
22246
22777
  watch
22247
22778
  } from "fs";
22248
22779
  import { createConnection } from "net";
22249
- import { createServer } from "http";
22250
- import { basename as basename14, dirname as dirname28, join as join48 } from "path";
22251
- import { execFile as execFile3, spawn as spawn10 } from "child_process";
22780
+ import { createServer as createServer2 } from "http";
22781
+ import { basename as basename14, dirname as dirname29, join as join48 } from "path";
22782
+ import { execFile as execFile3, spawn as spawn11 } from "child_process";
22252
22783
 
22253
22784
  // src/periscope-control.ts
22254
- import { spawn as spawn9 } from "child_process";
22785
+ import { spawn as spawn10 } from "child_process";
22255
22786
  var reply = (status2, body) => ({ status: status2, body });
22256
22787
  var err = (status2, message) => reply(status2, { error: message });
22257
22788
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -22442,7 +22973,7 @@ async function handlePeriscopes(h, q2) {
22442
22973
  }
22443
22974
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
22444
22975
  return new Promise((resolve8) => {
22445
- const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
22976
+ const child = spawn10(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
22446
22977
  let stdout = "";
22447
22978
  let stderr = "";
22448
22979
  let pending = "";
@@ -22481,7 +23012,7 @@ var MAX_SOCKET_PATH = 100;
22481
23012
  var MAX_TAIL_BYTES = 1024 * 1024;
22482
23013
  var MAX_WAIT_SECONDS = 30;
22483
23014
  var DEFAULT_TAIL_LINES = 100;
22484
- var MAX_BODY_BYTES = 4096;
23015
+ var MAX_BODY_BYTES2 = 4096;
22485
23016
  var ControlAuth = class {
22486
23017
  cache = /* @__PURE__ */ new Map();
22487
23018
  o;
@@ -22563,7 +23094,7 @@ function candidates(live, rotated) {
22563
23094
  const out = [live];
22564
23095
  if (!rotated) return out;
22565
23096
  try {
22566
- for (const n of readdirSync18(dirname28(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join48(dirname28(live), n));
23097
+ for (const n of readdirSync18(dirname29(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join48(dirname29(live), n));
22567
23098
  } catch {
22568
23099
  }
22569
23100
  return out;
@@ -22702,7 +23233,7 @@ async function readJsonBody(req) {
22702
23233
  let n = 0;
22703
23234
  for await (const c of req) {
22704
23235
  n += c.length;
22705
- if (n > MAX_BODY_BYTES) return null;
23236
+ if (n > MAX_BODY_BYTES2) return null;
22706
23237
  chunks.push(c);
22707
23238
  }
22708
23239
  if (!n) return {};
@@ -22723,7 +23254,7 @@ async function tailReply(path, q2, rotated, abort) {
22723
23254
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
22724
23255
  let r = readTail(path, { after, tail: tail3, rotated });
22725
23256
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
22726
- await waitForChange(dirname28(path), deadline - Date.now(), abort);
23257
+ await waitForChange(dirname29(path), deadline - Date.now(), abort);
22727
23258
  r = readTail(path, { after: r.cursor, rotated });
22728
23259
  }
22729
23260
  return reply2(200, r);
@@ -22831,7 +23362,7 @@ async function startControlServer(deps) {
22831
23362
  log(`control API not started: ${path} is in use by another process`);
22832
23363
  return null;
22833
23364
  }
22834
- const server = createServer((req, res) => {
23365
+ const server = createServer2((req, res) => {
22835
23366
  const abort = new AbortController();
22836
23367
  res.on("close", () => abort.abort());
22837
23368
  void (async () => {
@@ -22882,7 +23413,7 @@ async function startControlServer(deps) {
22882
23413
  });
22883
23414
  }
22884
23415
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
22885
- function runDetached(commands, spawnFn = spawn10) {
23416
+ function runDetached(commands, spawnFn = spawn11) {
22886
23417
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
22887
23418
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
22888
23419
  child.on("error", () => {
@@ -22913,21 +23444,21 @@ function crewJson(args, timeoutMs = 6e4) {
22913
23444
 
22914
23445
  // src/periscope-mcp.ts
22915
23446
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
22916
- import { basename as basename15, dirname as dirname29, isAbsolute as isAbsolute4, join as join50, relative as relative2, resolve as resolve6, sep } from "path";
23447
+ import { basename as basename15, dirname as dirname30, isAbsolute as isAbsolute5, join as join50, relative as relative2, resolve as resolve6, sep } from "path";
22917
23448
  import { z } from "zod";
22918
23449
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22919
23450
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22920
23451
 
22921
23452
  // src/periscope-endpoints.ts
22922
23453
  import { randomBytes as randomBytes3 } from "crypto";
22923
- import { chmodSync as chmodSync5, existsSync as existsSync46, mkdirSync as mkdirSync33, readdirSync as readdirSync19, readFileSync as readFileSync41, rmSync as rmSync22, writeFileSync as writeFileSync34 } from "fs";
23454
+ import { chmodSync as chmodSync5, existsSync as existsSync46, mkdirSync as mkdirSync34, readdirSync as readdirSync19, readFileSync as readFileSync41, rmSync as rmSync22, writeFileSync as writeFileSync34 } from "fs";
22924
23455
  import { join as join49 } from "path";
22925
23456
  var secretDir = (stateDir) => join49(stateDir, "periscope-mcp");
22926
23457
  var secretFile = (stateDir, id) => join49(secretDir(stateDir), `${id}.json`);
22927
23458
  var endpointDir = (stateDir) => join49(stateDir, "periscope-endpoints");
22928
23459
  var endpointFile = (stateDir, id) => join49(endpointDir(stateDir), `${id}.json`);
22929
23460
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
22930
- mkdirSync33(secretDir(stateDir), { recursive: true, mode: 448 });
23461
+ mkdirSync34(secretDir(stateDir), { recursive: true, mode: 448 });
22931
23462
  const secret = randomBytes3(32).toString("base64url");
22932
23463
  const file = secretFile(stateDir, ticketRecordId);
22933
23464
  writeFileSync34(file, `${JSON.stringify({ secret })}
@@ -22954,7 +23485,7 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
22954
23485
  }
22955
23486
  }
22956
23487
  function writePeriscopeEndpoint(stateDir, rec) {
22957
- mkdirSync33(endpointDir(stateDir), { recursive: true });
23488
+ mkdirSync34(endpointDir(stateDir), { recursive: true });
22958
23489
  writeFileSync34(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
22959
23490
  `);
22960
23491
  }
@@ -23024,7 +23555,7 @@ async function removeEndedPeriscopeEndpoints(d) {
23024
23555
  var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
23025
23556
  var MAX_READ_BYTES = 1024 * 1024;
23026
23557
  var MAX_WRITE_BYTES = 4 * 1024 * 1024;
23027
- var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
23558
+ var MAX_BODY_BYTES3 = 8 * 1024 * 1024;
23028
23559
  var MAX_HOOK_OUTPUT = 2e4;
23029
23560
  var MAX_DIFF = 2e5;
23030
23561
  var MAX_SEARCH_RESULTS = 500;
@@ -23032,7 +23563,7 @@ var PathRefusal = class extends Error {
23032
23563
  };
23033
23564
  var isInside = (root, p) => {
23034
23565
  const rel = relative2(root, p);
23035
- return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute4(rel);
23566
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute5(rel);
23036
23567
  };
23037
23568
  async function realAncestor(p) {
23038
23569
  let cur = p;
@@ -23051,10 +23582,10 @@ async function realAncestor(p) {
23051
23582
  }
23052
23583
  if (isLink) {
23053
23584
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
23054
- cur = resolve6(dirname29(cur), await readlink(cur));
23585
+ cur = resolve6(dirname30(cur), await readlink(cur));
23055
23586
  continue;
23056
23587
  }
23057
- const parent = dirname29(cur);
23588
+ const parent = dirname30(cur);
23058
23589
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
23059
23590
  tail3.push(basename15(cur));
23060
23591
  cur = parent;
@@ -23064,7 +23595,7 @@ async function resolveInWorktree(root, requested) {
23064
23595
  if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
23065
23596
  const realRoot = await realpath(root);
23066
23597
  let abs;
23067
- if (isAbsolute4(requested)) {
23598
+ if (isAbsolute5(requested)) {
23068
23599
  abs = resolve6(requested);
23069
23600
  if (isInside(root, abs)) abs = join50(realRoot, relative2(root, abs));
23070
23601
  } else {
@@ -23165,7 +23696,7 @@ function createPeriscopeMcpServer(d, sc) {
23165
23696
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
23166
23697
  const abs = await inTree(path);
23167
23698
  if (abs === await realpath(root)) return text("path must name a file", true);
23168
- await mkdir(dirname29(abs), { recursive: true });
23699
+ await mkdir(dirname30(abs), { recursive: true });
23169
23700
  await inTree(path);
23170
23701
  await writeFile(abs, content);
23171
23702
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -23205,7 +23736,7 @@ function createPeriscopeMcpServer(d, sc) {
23205
23736
  inputSchema: { path: z.string() },
23206
23737
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
23207
23738
  }, async ({ path }) => {
23208
- const parent = await inTree(dirname29(path) === "" ? "." : dirname29(path));
23739
+ const parent = await inTree(dirname30(path) === "" ? "." : dirname30(path));
23209
23740
  const abs = join50(parent, basename15(path));
23210
23741
  if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
23211
23742
  const st = await lstat(abs).catch(() => void 0);
@@ -23278,7 +23809,7 @@ async function readBody(req) {
23278
23809
  let n = 0;
23279
23810
  for await (const c of req) {
23280
23811
  n += c.length;
23281
- if (n > MAX_BODY_BYTES2) return null;
23812
+ if (n > MAX_BODY_BYTES3) return null;
23282
23813
  chunks.push(c);
23283
23814
  }
23284
23815
  return Buffer.concat(chunks).toString("utf8");
@@ -23327,7 +23858,7 @@ function findPeriscope(sidecars, segment) {
23327
23858
  }
23328
23859
 
23329
23860
  // src/cli.ts
23330
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname30(process.execPath) : resolve7(dirname30(fileURLToPath(import.meta.url)), "..");
23861
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname31(process.execPath) : resolve7(dirname31(fileURLToPath(import.meta.url)), "..");
23331
23862
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
23332
23863
  var CYCLE_SECONDS2 = 120;
23333
23864
  var STALE_CYCLES = 3;
@@ -23526,7 +24057,7 @@ function usage(problem, requested = false) {
23526
24057
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
23527
24058
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
23528
24059
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
23529
- crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
24060
+ crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket; --before-edit sorts by searches before the first edit
23530
24061
  crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
23531
24062
  crew passengers [route] sync Host Passengers containers/tunnels once; what the passengers unit invokes
23532
24063
  crew install write and load this platform's scheduler unit
@@ -23584,6 +24115,7 @@ var connectChoices = parseChoices(argv);
23584
24115
  if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
23585
24116
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
23586
24117
  "passenger-sync-daemon",
24118
+ "periscope-proxy",
23587
24119
  "agents",
23588
24120
  "connect",
23589
24121
  "credential",
@@ -23722,6 +24254,13 @@ if (command === "pool-member") {
23722
24254
  }
23723
24255
  if (command === "passenger-sync-daemon") {
23724
24256
  runSyncDaemonFromEnv();
24257
+ } else if (command === "periscope-proxy") {
24258
+ const dropFile = positional[1];
24259
+ if (!dropFile) {
24260
+ process.stderr.write("crew periscope-proxy is started by `crew periscope up`, not run by hand\n");
24261
+ process.exit(2);
24262
+ }
24263
+ runProxyFromEnv(dropFile);
23725
24264
  } else {
23726
24265
  let requireArmed = function(what) {
23727
24266
  if (dryRun || route2.enabled) return;
@@ -25763,7 +26302,7 @@ ${cu.tail.trimEnd()}
25763
26302
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
25764
26303
  const raw = existsSync47(resolvedPath) ? JSON.parse(readFileSync42(resolvedPath, "utf8")) : {};
25765
26304
  raw.agentSkills = result.agentSkills;
25766
- mkdirSync34(dirOf2(resolvedPath), { recursive: true });
26305
+ mkdirSync35(dirOf2(resolvedPath), { recursive: true });
25767
26306
  writeFileSync35(resolvedPath, `${JSON.stringify(raw, null, 2)}
25768
26307
  `);
25769
26308
  }
@@ -25841,7 +26380,7 @@ ${cu.tail.trimEnd()}
25841
26380
  const found = cfg.routes.find((r) => r.route === routeArg);
25842
26381
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
25843
26382
  const named2 = value("route") ? void 0 : positional[3];
25844
- const dir = named2 ? repoDirOf(configText, dirname30(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
26383
+ const dir = named2 ? repoDirOf(configText, dirname31(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
25845
26384
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
25846
26385
  const o = target ? found.repoOverrides[target.name] : void 0;
25847
26386
  runReposCheck(dir, {
@@ -25864,7 +26403,7 @@ ${cu.tail.trimEnd()}
25864
26403
  if (sub === "hooks") {
25865
26404
  try {
25866
26405
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
25867
- const dir = repoDirOf(configText, dirname30(cfg.configFile), a.route, a.name);
26406
+ const dir = repoDirOf(configText, dirname31(cfg.configFile), a.route, a.name);
25868
26407
  if (!existsSync47(dir)) throw new ReposError(`${dir} does not exist on disk`);
25869
26408
  const files = { exists: (r) => existsSync47(resolvePath(dir, r)), list: (r) => {
25870
26409
  try {
@@ -26031,7 +26570,7 @@ ${cu.tail.trimEnd()}
26031
26570
  }
26032
26571
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
26033
26572
  const text2 = configText;
26034
- const base = dirname30(cfg.configFile);
26573
+ const base = dirname31(cfg.configFile);
26035
26574
  const isCheckout = (d) => existsSync47(`${d}/.git`);
26036
26575
  try {
26037
26576
  if (sub === "list") {
@@ -26179,7 +26718,7 @@ ${cu.tail.trimEnd()}
26179
26718
  const sub = positional[1];
26180
26719
  if (sub !== "list" && sub !== "show") {
26181
26720
  process.stderr.write(
26182
- "crew logbook list [route] [--role R] [--ticket T] [--limit N]\ncrew logbook show [route] <entryId> [--prompt]\n"
26721
+ "crew logbook list [route] [--role R] [--ticket T] [--limit N] [--before-edit]\ncrew logbook show [route] <entryId> [--prompt]\n"
26183
26722
  );
26184
26723
  process.exit(2);
26185
26724
  }
@@ -26206,7 +26745,8 @@ ${cu.tail.trimEnd()}
26206
26745
  userAgent: cfg.ship.userAgent,
26207
26746
  role: value("role"),
26208
26747
  ticket: value("ticket"),
26209
- limit: value("limit") ? Number(value("limit")) : void 0
26748
+ limit: value("limit") ? Number(value("limit")) : void 0,
26749
+ beforeEdit: flag("before-edit")
26210
26750
  });
26211
26751
  if (entries.length === 0) {
26212
26752
  process.stdout.write("(no Agent Log entries match)\n");
@@ -26214,7 +26754,7 @@ ${cu.tail.trimEnd()}
26214
26754
  }
26215
26755
  for (const e of entries) {
26216
26756
  process.stdout.write(
26217
- `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}
26757
+ `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}${formatBeforeEdit(e) ? ` before edit: ${formatBeforeEdit(e)}` : ""}
26218
26758
  `
26219
26759
  );
26220
26760
  }
@@ -26234,6 +26774,7 @@ harness: ${entry.harness ?? "?"}
26234
26774
  provider: ${entry.provider ?? "?"}
26235
26775
  ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
26236
26776
  ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
26777
+ ` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
26237
26778
  ` : "")
26238
26779
  );
26239
26780
  if (flag("prompt")) {
@@ -26707,7 +27248,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26707
27248
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
26708
27249
  `);
26709
27250
  } else {
26710
- mkdirSync34(dirname30(resolvedPath), { recursive: true });
27251
+ mkdirSync35(dirname31(resolvedPath), { recursive: true });
26711
27252
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
26712
27253
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
26713
27254
  writeFileSync35(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -26756,7 +27297,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26756
27297
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
26757
27298
  `);
26758
27299
  } else {
26759
- mkdirSync34(dirname30(apiKeyFile), { recursive: true });
27300
+ mkdirSync35(dirname31(apiKeyFile), { recursive: true });
26760
27301
  writeFileSync35(apiKeyFile, `${apiKeyVar}=${apiKey}
26761
27302
  `, { mode: 384 });
26762
27303
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -27634,6 +28175,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
27634
28175
  find: async () => hit && { row: hit.row, tag: hit.tag },
27635
28176
  worktreeExists: (p) => existsSync47(p),
27636
28177
  prepareDropFile,
28178
+ startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: hit.row.host || "localhost", inject: hit.repo.config.periscope?.inspector !== "repo" }),
27637
28179
  caps: periscopeCaps(cfg.ship),
27638
28180
  runHook: async (name, cwd, env) => {
27639
28181
  const script = hit?.repo.config.hooks[name];
@@ -27729,7 +28271,7 @@ ${where.join("\n")}`);
27729
28271
  for (const f of repoP.config.worktrees.copy) {
27730
28272
  const src = join51(repoP.dir, f);
27731
28273
  if (!existsSync47(src)) continue;
27732
- mkdirSync34(dirOf2(join51(path, f)), { recursive: true });
28274
+ mkdirSync35(dirOf2(join51(path, f)), { recursive: true });
27733
28275
  copyFileSync2(src, join51(path, f));
27734
28276
  }
27735
28277
  const setup = repoP.config.hooks.setup;
@@ -27743,6 +28285,7 @@ ${where.join("\n")}`);
27743
28285
  },
27744
28286
  createRow: (fields) => trackerP.createPeriscope(fields),
27745
28287
  prepareDropFile,
28288
+ startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: a.host ?? "localhost", inject: repoP.config.periscope?.inspector !== "repo" }),
27746
28289
  registerCode: registerCodeServer,
27747
28290
  recordLocal: (sc) => {
27748
28291
  writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
@@ -28081,7 +28624,7 @@ ${where.join("\n")}`);
28081
28624
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
28082
28625
  break;
28083
28626
  }
28084
- mkdirSync34(cfg.ship.stateDir, { recursive: true });
28627
+ mkdirSync35(cfg.ship.stateDir, { recursive: true });
28085
28628
  writeFileSync35(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
28086
28629
  `);
28087
28630
  process.stdout.write(
@@ -28171,7 +28714,7 @@ ${where.join("\n")}`);
28171
28714
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
28172
28715
  const tracker = new Tracker(route2, cfg.ship);
28173
28716
  const configText = readFileSync42(cfg.configFile, "utf8");
28174
- const cfgBase = dirname30(cfg.configFile);
28717
+ const cfgBase = dirname31(cfg.configFile);
28175
28718
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
28176
28719
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
28177
28720
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -28227,7 +28770,7 @@ ${where.join("\n")}`);
28227
28770
  break;
28228
28771
  }
28229
28772
  if (fresh) {
28230
- mkdirSync34(dir, { recursive: true });
28773
+ mkdirSync35(dir, { recursive: true });
28231
28774
  writeFileSync35(join51(dir, "README.md"), SAMPLE_README);
28232
28775
  writeFileSync35(join51(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
28233
28776
  const g = (...a) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.117.1",
3
+ "version": "0.119.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",