@tablation/crew 0.7.1 → 0.9.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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +272 -68
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -213,8 +213,8 @@ crew inbox [--member NAME] your tickets across every workspace
213
213
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
214
214
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
215
215
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
216
- crew install write and load this platform's scheduler unit
217
- crew uninstall unload and remove it
216
+ crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
217
+ crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
218
218
  ```
219
219
 
220
220
  Every command that could change something takes `--dry-run`.
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname9, join as join18, basename as basename7 } from "path";
4
+ import { resolve as resolve4, dirname as dirname9, join as join19, basename as basename7 } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { hostname as hostname2, homedir as homedir4 } from "os";
7
- import { spawnSync as spawnSync2 } from "child_process";
6
+ import { hostname as hostname2, homedir as homedir5 } from "os";
7
+ import { spawnSync as spawnSync3 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
10
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
@@ -1930,7 +1930,7 @@ function describeRelease(o, route) {
1930
1930
  // src/install.ts
1931
1931
  import { dirname as dirname3, join as join5, extname } from "path";
1932
1932
  import { homedir as homedir2 } from "os";
1933
- import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
1933
+ import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
1934
1934
  import { spawnSync } from "child_process";
1935
1935
  import { createHash } from "crypto";
1936
1936
 
@@ -2164,7 +2164,8 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2164
2164
  mechanism: "launchd",
2165
2165
  unitPaths: [unitPath],
2166
2166
  crontabLine: null,
2167
- unloadCommands: [["launchctl", "unload", unitPath]]
2167
+ unloadCommands: [["launchctl", "unload", unitPath]],
2168
+ removeLabel: label
2168
2169
  };
2169
2170
  }
2170
2171
  if (hostShip === "linux" && hasSystemd) {
@@ -2282,6 +2283,15 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2282
2283
  if (!ok) log.warn("unload command failed \u2014 it may not have been loaded; continuing to remove files");
2283
2284
  }
2284
2285
  }
2286
+ if (plan.removeLabel) {
2287
+ if (dryRun) {
2288
+ log.emit(`would run: launchctl remove ${plan.removeLabel}`);
2289
+ } else {
2290
+ if (spawnSync("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
2291
+ log.emit(`removed loaded job ${plan.removeLabel}`);
2292
+ }
2293
+ }
2294
+ }
2285
2295
  for (const path of plan.unitPaths) {
2286
2296
  if (!existsSync5(path)) continue;
2287
2297
  if (dryRun) {
@@ -2300,6 +2310,91 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2300
2310
  }
2301
2311
  }
2302
2312
  }
2313
+ var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
2314
+ function launchAgentsDir() {
2315
+ return join5(homedir2(), "Library", "LaunchAgents");
2316
+ }
2317
+ function xmlUnescape(s) {
2318
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
2319
+ }
2320
+ function programFromPlist(text) {
2321
+ const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text);
2322
+ if (!block) return null;
2323
+ const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
2324
+ return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
2325
+ }
2326
+ function listLaunchdUnits(dir = launchAgentsDir()) {
2327
+ if (!existsSync5(dir)) return [];
2328
+ return readdirSync(dir).filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2329
+ const plistPath = join5(dir, f);
2330
+ let program = null;
2331
+ try {
2332
+ program = programFromPlist(readFileSync5(plistPath, "utf8"));
2333
+ } catch {
2334
+ }
2335
+ return {
2336
+ label: f.slice(0, -".plist".length),
2337
+ plistPath,
2338
+ program,
2339
+ programExists: program !== null && existsSync5(program)
2340
+ };
2341
+ });
2342
+ }
2343
+ function foreignLaunchdUnits(crewHome, dir) {
2344
+ const own = new Set(["run", "release", "passengers"].map((job) => labelFor(crewHome, job)));
2345
+ return listLaunchdUnits(dir).filter((u) => !own.has(u.label));
2346
+ }
2347
+ function removeLaunchdUnit(unit, dryRun, log) {
2348
+ if (dryRun) {
2349
+ log.emit(`would unload and remove ${unit.label}`);
2350
+ return;
2351
+ }
2352
+ if (existsSync5(unit.plistPath)) spawnSync("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
2353
+ spawnSync("launchctl", ["remove", unit.label], { encoding: "utf8" });
2354
+ if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
2355
+ log.emit(`removed ${unit.label} (${unit.plistPath})`);
2356
+ }
2357
+ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir) {
2358
+ const foreign = foreignLaunchdUnits(crewHome, dir);
2359
+ const blocking = foreign.filter((u) => u.program === null || u.programExists);
2360
+ if (blocking.length > 0 && !replace) {
2361
+ log.warn(
2362
+ `two crew installs target this machine: ${blocking.map((u) => `${u.label} runs ${u.program ?? "an unreadable ProgramArguments"}`).join("; ")}. Run \`crew uninstall\` from the other one, or pass --replace.`
2363
+ );
2364
+ return false;
2365
+ }
2366
+ for (const unit of foreign) {
2367
+ const why = unit.program === null ? "replacing a unit with no readable ProgramArguments" : unit.programExists ? "replacing a live other install" : "removing a stale unit whose program is gone";
2368
+ log.emit(`${why}: ${unit.label}`);
2369
+ removeLaunchdUnit(unit, dryRun, log);
2370
+ }
2371
+ return true;
2372
+ }
2373
+ function removeAllLaunchdUnits(dryRun, log, dir) {
2374
+ for (const unit of listLaunchdUnits(dir)) removeLaunchdUnit(unit, dryRun, log);
2375
+ }
2376
+ function parseLaunchctlList(text) {
2377
+ const jobs = /* @__PURE__ */ new Map();
2378
+ for (const line of text.split("\n")) {
2379
+ const cols = line.split(" ");
2380
+ if (cols.length !== 3 || cols[2] === "Label") continue;
2381
+ jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
2382
+ }
2383
+ return jobs;
2384
+ }
2385
+ function describeForeignLaunchdUnits(crewHome, dir) {
2386
+ const foreign = foreignLaunchdUnits(crewHome, dir);
2387
+ if (foreign.length === 0) return [];
2388
+ const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
2389
+ const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
2390
+ return foreign.map((u) => {
2391
+ const status2 = jobs.get(u.label);
2392
+ const loaded = status2 ? `loaded, last exit ${status2.lastExit}` : "not loaded";
2393
+ const program = u.program ? `${u.program} (${u.programExists ? "present" : "GONE"})` : "no readable ProgramArguments";
2394
+ const stale = status2 && !u.programExists ? " \u2014 stale: `crew install` removes it" : "";
2395
+ return ` ${u.label}: ${loaded}; runs ${program}${stale}`;
2396
+ });
2397
+ }
2303
2398
  function readCrontab() {
2304
2399
  const res = spawnSync("crontab", ["-l"], { encoding: "utf8" });
2305
2400
  return res.status === 0 ? res.stdout ?? "" : "";
@@ -4511,7 +4606,7 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
4511
4606
 
4512
4607
  // src/agent.ts
4513
4608
  import { spawn as spawn2 } from "child_process";
4514
- import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
4609
+ import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
4515
4610
  import { finished } from "stream/promises";
4516
4611
  import { join as join7, dirname as dirname5 } from "path";
4517
4612
  import { createHash as createHash2 } from "crypto";
@@ -4954,10 +5049,10 @@ function planAgentRun(o) {
4954
5049
  let ageSeconds;
4955
5050
  if (existsSync7(digestPath2)) {
4956
5051
  ageSeconds = Math.floor((now() - statSync4(digestPath2).mtimeMs) / 1e3);
4957
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync5(digestPath2, "utf8");
5052
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync6(digestPath2, "utf8");
4958
5053
  }
4959
- const common = readFileSync5(commonPath, "utf8");
4960
- const brief = readFileSync5(briefPath, "utf8");
5054
+ const common = readFileSync6(commonPath, "utf8");
5055
+ const brief = readFileSync6(briefPath, "utf8");
4961
5056
  const effectiveCommon = o.divergedPrompt ? "" : common;
4962
5057
  const effectiveBrief = o.divergedPrompt ?? brief;
4963
5058
  const prompt = assemblePrompt({
@@ -5337,7 +5432,7 @@ Wrote ${o.configPath}.
5337
5432
  }
5338
5433
 
5339
5434
  // src/release-run.ts
5340
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
5435
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
5341
5436
  import { join as join9 } from "path";
5342
5437
 
5343
5438
  // src/release.ts
@@ -5802,7 +5897,7 @@ async function bump(o, size) {
5802
5897
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
5803
5898
  return null;
5804
5899
  }
5805
- const current = JSON.parse(readFileSync6(first, "utf8")).version;
5900
+ const current = JSON.parse(readFileSync7(first, "utf8")).version;
5806
5901
  if (!current) {
5807
5902
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
5808
5903
  return null;
@@ -5815,7 +5910,7 @@ async function bump(o, size) {
5815
5910
  return version;
5816
5911
  }
5817
5912
  for (const f of files) {
5818
- const src = readFileSync6(f, "utf8");
5913
+ const src = readFileSync7(f, "utf8");
5819
5914
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
5820
5915
  if (out === src) {
5821
5916
  o.emit.error(`no version field in ${f}`);
@@ -5834,7 +5929,7 @@ function writeChangelog(o, version, entries) {
5834
5929
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
5835
5930
  return;
5836
5931
  }
5837
- const existing = existsSync9(path) ? readFileSync6(path, "utf8") : "# Changelog\n";
5932
+ const existing = existsSync9(path) ? readFileSync7(path, "utf8") : "# Changelog\n";
5838
5933
  writeFileSync6(path, insertChangelogSection(existing, section));
5839
5934
  }
5840
5935
  async function confirm(o, expected) {
@@ -6682,7 +6777,7 @@ function nextRoles(decision, ran) {
6682
6777
  }
6683
6778
 
6684
6779
  // src/agents.ts
6685
- import { readFileSync as readFileSync7, existsSync as existsSync12 } from "fs";
6780
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
6686
6781
  import { join as join11 } from "path";
6687
6782
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
6688
6783
  var PERSONA_NAME = {
@@ -6699,10 +6794,10 @@ var AgentsSyncError = class extends Error {
6699
6794
  function personaDefaultPrompt(promptsDir, role) {
6700
6795
  const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
6701
6796
  if (!existsSync12(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
6702
- if (role === "pair") return readFileSync7(briefPath, "utf8");
6797
+ if (role === "pair") return readFileSync8(briefPath, "utf8");
6703
6798
  const commonPath = join11(promptsDir, "personas", "common.md");
6704
6799
  if (!existsSync12(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
6705
- return `${readFileSync7(commonPath, "utf8")}${readFileSync7(briefPath, "utf8")}`;
6800
+ return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
6706
6801
  }
6707
6802
  async function resolveAgentId(route, role, opts) {
6708
6803
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -7323,14 +7418,14 @@ import {
7323
7418
  } from "@tablation/client";
7324
7419
 
7325
7420
  // src/skills.ts
7326
- import { readFileSync as readFileSync8, readdirSync, existsSync as existsSync13 } from "fs";
7421
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync13 } from "fs";
7327
7422
  import { join as join12 } from "path";
7328
7423
  import { parse as parseYaml } from "yaml";
7329
7424
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
7330
7425
  var SkillSyncError = class extends Error {
7331
7426
  };
7332
7427
  function parseSkillFile(path) {
7333
- const text = readFileSync8(path, "utf8");
7428
+ const text = readFileSync9(path, "utf8");
7334
7429
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7335
7430
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7336
7431
  const frontmatter = match[1];
@@ -7345,7 +7440,7 @@ function parseSkillFile(path) {
7345
7440
  function listLocalSkills(promptsDir) {
7346
7441
  const dir = join12(promptsDir, "skills");
7347
7442
  if (!existsSync13(dir)) return [];
7348
- return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7443
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7349
7444
  }
7350
7445
  async function syncSkills(route, opts) {
7351
7446
  if (!route.resolved) {
@@ -7668,6 +7763,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
7668
7763
  );
7669
7764
  }
7670
7765
 
7766
+ // src/claude-mcp.ts
7767
+ import { spawnSync as spawnSync2 } from "child_process";
7768
+ import { readFileSync as readFileSync10 } from "fs";
7769
+ import { homedir as homedir4 } from "os";
7770
+ import { join as join13 } from "path";
7771
+ var MCP_SERVER_NAME = "tablation";
7772
+ var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
7773
+ function mcpUrlFor(baseUrl) {
7774
+ return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
7775
+ }
7776
+ function runClaude(bin, args) {
7777
+ const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
7778
+ const missing = r.error?.code === "ENOENT";
7779
+ return { status: r.status, missing, stderr: r.stderr ?? "" };
7780
+ }
7781
+ function userScopeEntry(home) {
7782
+ try {
7783
+ const j = JSON.parse(readFileSync10(join13(home, ".claude.json"), "utf8"));
7784
+ return j.mcpServers?.[MCP_SERVER_NAME];
7785
+ } catch {
7786
+ return void 0;
7787
+ }
7788
+ }
7789
+ function inspectClaudeMcp(o) {
7790
+ const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
7791
+ if (got.missing) return { state: "no-claude" };
7792
+ if (got.status !== 0) return { state: "missing" };
7793
+ const entry = userScopeEntry(o.home ?? homedir4());
7794
+ if (!entry) {
7795
+ return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
7796
+ }
7797
+ if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
7798
+ const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
7799
+ if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
7800
+ return { state: "registered", url: o.url };
7801
+ }
7802
+ function ensureClaudeMcp(o) {
7803
+ const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
7804
+ const st = inspectClaudeMcp(o);
7805
+ if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
7806
+ if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
7807
+ if (st.state === "different" && !o.replace) {
7808
+ return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
7809
+ }
7810
+ if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
7811
+ if (st.state === "different") {
7812
+ runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
7813
+ }
7814
+ const add = runClaude(o.bin, [
7815
+ "mcp",
7816
+ "add",
7817
+ "--transport",
7818
+ "http",
7819
+ MCP_SERVER_NAME,
7820
+ o.url,
7821
+ "--header",
7822
+ `Authorization: Bearer ${o.key}`,
7823
+ "--scope",
7824
+ "user"
7825
+ ]);
7826
+ if (add.status !== 0) {
7827
+ return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
7828
+ }
7829
+ return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
7830
+ }
7831
+
7671
7832
  // src/doctor-fix.ts
7672
7833
  import { parseDocument as parseDocument2 } from "yaml";
7673
7834
  var sound = (r) => r.hasRepos && r.problems.length === 0;
@@ -7818,14 +7979,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
7818
7979
  }
7819
7980
 
7820
7981
  // src/stream-sweep.ts
7821
- import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
7822
- import { join as join13 } from "path";
7982
+ import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync2, statSync as statSync6 } from "fs";
7983
+ import { join as join14 } from "path";
7823
7984
  var DAY_MS = 24 * 60 * 60 * 1e3;
7824
7985
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
7825
- const dir = join13(stateDir, "streams");
7986
+ const dir = join14(stateDir, "streams");
7826
7987
  if (!existsSync15(dir)) return [];
7827
7988
  const cutoff = now - retentionDays * DAY_MS;
7828
- return readdirSync2(dir).map((name) => join13(dir, name)).filter((path) => {
7989
+ return readdirSync3(dir).map((name) => join14(dir, name)).filter((path) => {
7829
7990
  try {
7830
7991
  return statSync6(path).mtimeMs < cutoff;
7831
7992
  } catch {
@@ -7852,18 +8013,18 @@ function applyStreamSweep(paths, dryRun, log) {
7852
8013
  }
7853
8014
 
7854
8015
  // src/cli.ts
7855
- import { readFileSync as readFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
8016
+ import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
7856
8017
  import { createInterface } from "readline/promises";
7857
8018
  import { dirname as dirOf, resolve as resolvePath } from "path";
7858
8019
 
7859
8020
  // src/ssh-keys.ts
7860
8021
  import { execFileSync as execFileSync3 } from "child_process";
7861
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync9, chmodSync } from "fs";
7862
- import { dirname as dirname8, join as join14 } from "path";
8022
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, chmodSync } from "fs";
8023
+ import { dirname as dirname8, join as join15 } from "path";
7863
8024
  var SshKeyError = class extends Error {
7864
8025
  };
7865
8026
  function shipSshKeyPathFor(stateDir) {
7866
- return join14(stateDir, "ssh", "id_ed25519");
8027
+ return join15(stateDir, "ssh", "id_ed25519");
7867
8028
  }
7868
8029
  function ensureShipSshKeypair(stateDir) {
7869
8030
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -7892,7 +8053,7 @@ function ensureShipSshKeypair(stateDir) {
7892
8053
  if (!existsSync16(publicKeyPath)) {
7893
8054
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
7894
8055
  }
7895
- const publicKey = readFileSync9(publicKeyPath, "utf8").trim();
8056
+ const publicKey = readFileSync11(publicKeyPath, "utf8").trim();
7896
8057
  return { privateKeyPath, publicKeyPath, publicKey };
7897
8058
  }
7898
8059
  function sshKeygenAvailable() {
@@ -8106,9 +8267,9 @@ function syncAllPassengerCheckouts(cfg) {
8106
8267
 
8107
8268
  // src/tunnel.ts
8108
8269
  import { spawn as spawn3 } from "child_process";
8109
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
8270
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
8110
8271
  import { randomInt } from "crypto";
8111
- import { join as join15 } from "path";
8272
+ import { join as join16 } from "path";
8112
8273
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
8113
8274
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
8114
8275
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -8175,20 +8336,20 @@ function startTunnel(opts) {
8175
8336
  };
8176
8337
  }
8177
8338
  function tunnelStatePath(stateDir, workspaceId) {
8178
- return join15(stateDir, "passengers", `${workspaceId}.json`);
8339
+ return join16(stateDir, "passengers", `${workspaceId}.json`);
8179
8340
  }
8180
8341
  function readPersistedTunnel(stateDir, workspaceId) {
8181
8342
  const path = tunnelStatePath(stateDir, workspaceId);
8182
8343
  if (!existsSync17(path)) return void 0;
8183
8344
  try {
8184
- return JSON.parse(readFileSync10(path, "utf8"));
8345
+ return JSON.parse(readFileSync12(path, "utf8"));
8185
8346
  } catch {
8186
8347
  return void 0;
8187
8348
  }
8188
8349
  }
8189
8350
  function writePersistedTunnel(stateDir, state) {
8190
8351
  const path = tunnelStatePath(stateDir, state.workspaceId);
8191
- mkdirSync8(join15(stateDir, "passengers"), { recursive: true });
8352
+ mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
8192
8353
  writeFileSync7(path, `${JSON.stringify(state, null, 2)}
8193
8354
  `);
8194
8355
  }
@@ -8286,9 +8447,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
8286
8447
  }
8287
8448
  }
8288
8449
  function readAllPersistedWorkspaceIds(stateDir) {
8289
- const dir = join15(stateDir, "passengers");
8450
+ const dir = join16(stateDir, "passengers");
8290
8451
  if (!existsSync17(dir)) return [];
8291
- return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8452
+ return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8292
8453
  }
8293
8454
 
8294
8455
  // src/mcp-endpoint-registry.ts
@@ -8408,8 +8569,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
8408
8569
  import { createServer as createHttpServer } from "http";
8409
8570
  import { spawn as spawn4 } from "child_process";
8410
8571
  import { randomBytes } from "crypto";
8411
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync4 } from "fs";
8412
- import { join as join16 } from "path";
8572
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
8573
+ import { join as join17 } from "path";
8413
8574
 
8414
8575
  // src/secret-compare.ts
8415
8576
  import { timingSafeEqual } from "crypto";
@@ -8498,20 +8659,20 @@ var defaultSpawn2 = (cmd, args, env) => {
8498
8659
  return child;
8499
8660
  };
8500
8661
  function syncDaemonStatePath(stateDir, workspaceId) {
8501
- return join16(stateDir, "passengers", `${workspaceId}-sync.json`);
8662
+ return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
8502
8663
  }
8503
8664
  function readPersistedSyncDaemon(stateDir, workspaceId) {
8504
8665
  const path = syncDaemonStatePath(stateDir, workspaceId);
8505
8666
  if (!existsSync18(path)) return void 0;
8506
8667
  try {
8507
- return JSON.parse(readFileSync11(path, "utf8"));
8668
+ return JSON.parse(readFileSync13(path, "utf8"));
8508
8669
  } catch {
8509
8670
  return void 0;
8510
8671
  }
8511
8672
  }
8512
8673
  function writePersistedSyncDaemon(stateDir, state) {
8513
8674
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
8514
- mkdirSync9(join16(stateDir, "passengers"), { recursive: true });
8675
+ mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
8515
8676
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
8516
8677
  `);
8517
8678
  }
@@ -8532,10 +8693,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
8532
8693
  }
8533
8694
  }
8534
8695
  function readAllPersistedSyncWorkspaceIds(stateDir) {
8535
- const dir = join16(stateDir, "passengers");
8696
+ const dir = join17(stateDir, "passengers");
8536
8697
  if (!existsSync18(dir)) return [];
8537
8698
  const suffix = "-sync.json";
8538
- return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
8699
+ return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
8539
8700
  }
8540
8701
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8541
8702
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -8590,8 +8751,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8590
8751
  }
8591
8752
 
8592
8753
  // src/daemon.ts
8593
- import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
8594
- import { join as join17 } from "path";
8754
+ import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
8755
+ import { join as join18 } from "path";
8595
8756
  var DEFAULT_FLOOR_MS = 5e3;
8596
8757
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
8597
8758
  var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
@@ -8629,8 +8790,8 @@ function latestMtime(path) {
8629
8790
  if (stat.isFile()) return stat.mtimeMs;
8630
8791
  if (!stat.isDirectory()) return 0;
8631
8792
  let latest = stat.mtimeMs;
8632
- for (const entry of readdirSync5(path, { withFileTypes: true })) {
8633
- latest = Math.max(latest, latestMtime(join17(path, entry.name)));
8793
+ for (const entry of readdirSync6(path, { withFileTypes: true })) {
8794
+ latest = Math.max(latest, latestMtime(join18(path, entry.name)));
8634
8795
  }
8635
8796
  return latest;
8636
8797
  }
@@ -9006,7 +9167,7 @@ function usage() {
9006
9167
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
9007
9168
  crew log [route] tail the log
9008
9169
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
9009
- crew connect resolve a workspace's ids into a crew.yaml block
9170
+ crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
9010
9171
  crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)
9011
9172
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
9012
9173
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
@@ -9308,12 +9469,21 @@ if (command === "passenger-sync-daemon") {
9308
9469
  const trimmed = typeof raw === "string" ? raw.trim() : "";
9309
9470
  return trimmed ? `${trimmed.toLowerCase()}-` : void 0;
9310
9471
  }
9311
- async function installScheduler() {
9472
+ async function installScheduler(opts = {}) {
9312
9473
  const host = hostPlatform();
9313
9474
  if (host === "windows") {
9314
9475
  process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
9315
9476
  process.exit(2);
9316
9477
  }
9478
+ if (host === "macos") {
9479
+ const ok = clearForeignLaunchdUnits(CREW_HOME, !!opts.replace, dryRun, {
9480
+ emit: (m) => process.stdout.write(`${m}
9481
+ `),
9482
+ warn: (m) => process.stderr.write(`crew install: ${m}
9483
+ `)
9484
+ });
9485
+ if (!ok) return false;
9486
+ }
9317
9487
  const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
9318
9488
  if (installPathProblem) {
9319
9489
  process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
@@ -9350,14 +9520,14 @@ if (command === "passenger-sync-daemon") {
9350
9520
  }
9351
9521
  if (host === "linux" && hasSystemd) {
9352
9522
  const label = labelFor(CREW_HOME, "run");
9353
- const staleTimer = join18(homedir4(), ".config", "systemd", "user", `${label}.timer`);
9523
+ const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
9354
9524
  if (existsSync19(staleTimer)) {
9355
9525
  process.stdout.write(
9356
9526
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
9357
9527
  `
9358
9528
  );
9359
9529
  if (!dryRun) {
9360
- spawnSync2("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9530
+ spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9361
9531
  try {
9362
9532
  unlinkSync5(staleTimer);
9363
9533
  } catch {
@@ -9365,6 +9535,7 @@ if (command === "passenger-sync-daemon") {
9365
9535
  }
9366
9536
  }
9367
9537
  }
9538
+ return true;
9368
9539
  }
9369
9540
  async function routeProblems(c) {
9370
9541
  const host = hostPlatform();
@@ -9395,7 +9566,7 @@ if (command === "passenger-sync-daemon") {
9395
9566
  async function doctorFix(o) {
9396
9567
  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))));
9397
9568
  const unitExists = (p) => existsSync19(p);
9398
- const crontab = o.host === "linux" ? spawnSync2("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9569
+ const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9399
9570
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
9400
9571
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
9401
9572
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -9411,14 +9582,13 @@ if (command === "passenger-sync-daemon") {
9411
9582
  for (const a of plan.actions) {
9412
9583
  if (a.kind === "enable") {
9413
9584
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
9414
- writeFileSync9(cfg.configFile, planEnable(readFileSync12(cfg.configFile, "utf8"), a.route));
9585
+ writeFileSync9(cfg.configFile, planEnable(readFileSync14(cfg.configFile, "utf8"), a.route));
9415
9586
  enabledSound.add(a.route);
9416
9587
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
9417
9588
  } else {
9418
9589
  if (enabledSound.size === 0) continue;
9419
9590
  if (prompter && !await prompter.confirm("No scheduler is installed for this ship \u2014 run `crew install` now?", false)) continue;
9420
- await installScheduler();
9421
- changed2.push("ran `crew install` (scheduler units written and loaded)");
9591
+ if (await installScheduler()) changed2.push("ran `crew install` (scheduler units written and loaded)");
9422
9592
  }
9423
9593
  }
9424
9594
  } finally {
@@ -10121,7 +10291,7 @@ ${changed2.map((c) => ` - ${c}
10121
10291
  }
10122
10292
  let failed = false;
10123
10293
  for (const cmd2 of commands) {
10124
- const res2 = spawnSync2(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10294
+ const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10125
10295
  if (res2.stdout) process.stdout.write(res2.stdout);
10126
10296
  if (res2.stderr) process.stderr.write(res2.stderr);
10127
10297
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -10139,7 +10309,7 @@ ${changed2.map((c) => ` - ${c}
10139
10309
  `);
10140
10310
  break;
10141
10311
  }
10142
- const res = spawnSync2(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10312
+ const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10143
10313
  if (res.stdout) process.stdout.write(res.stdout);
10144
10314
  if (res.stderr) process.stderr.write(res.stderr);
10145
10315
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -10162,7 +10332,7 @@ ${changed2.map((c) => ` - ${c}
10162
10332
  process.exit(1);
10163
10333
  }
10164
10334
  requireArmed("run the daemon loop");
10165
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join18(CREW_HOME, "src"), join18(CREW_HOME, "dist"), join18(CREW_HOME, "bin")];
10335
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join19(CREW_HOME, "src"), join19(CREW_HOME, "dist"), join19(CREW_HOME, "bin")];
10166
10336
  const spawnBreaker = new SpawnBreaker();
10167
10337
  const stopReason = await runDaemonLoop({
10168
10338
  runPass: () => runOnePass({
@@ -10310,7 +10480,7 @@ ${changed2.map((c) => ` - ${c}
10310
10480
  `);
10311
10481
  if (!dryRun) {
10312
10482
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10313
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10483
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10314
10484
  raw.agentPersonas = personaResult.agentPersonas;
10315
10485
  raw.agentSkills = skillResult.agentSkills;
10316
10486
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
@@ -10359,7 +10529,7 @@ ${changed2.map((c) => ` - ${c}
10359
10529
  `);
10360
10530
  if (!dryRun) {
10361
10531
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10362
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10532
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10363
10533
  raw.agentSkills = result.agentSkills;
10364
10534
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
10365
10535
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -10375,14 +10545,14 @@ ${changed2.map((c) => ` - ${c}
10375
10545
  process.exit(2);
10376
10546
  };
10377
10547
  if (sub !== "add" && sub !== "list") usage2();
10378
- const configText = readFileSync12(cfg.configFile, "utf8");
10548
+ const configText = readFileSync14(cfg.configFile, "utf8");
10379
10549
  let routeName = positional[2];
10380
10550
  let addPath = ".";
10381
10551
  let addName;
10382
10552
  if (sub === "list" && !routeName) usage2();
10383
10553
  if (sub === "add") {
10384
10554
  try {
10385
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir4()))));
10555
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
10386
10556
  routeName = a.route;
10387
10557
  addPath = a.path;
10388
10558
  addName = a.name;
@@ -10399,7 +10569,7 @@ ${changed2.map((c) => ` - ${c}
10399
10569
  let resolvedRemotes;
10400
10570
  if (existsSync19(resolvedFile)) {
10401
10571
  try {
10402
- resolvedRemotes = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10572
+ resolvedRemotes = JSON.parse(readFileSync14(resolvedFile, "utf8"));
10403
10573
  } catch {
10404
10574
  }
10405
10575
  }
@@ -10420,7 +10590,7 @@ ${changed2.map((c) => ` - ${c}
10420
10590
  }
10421
10591
  break;
10422
10592
  }
10423
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir4()));
10593
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
10424
10594
  const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
10425
10595
  if (!top) throw new ReposError(`${target} is not a git checkout`);
10426
10596
  const origin = originOf(top);
@@ -10470,7 +10640,7 @@ ${changed2.map((c) => ` - ${c}
10470
10640
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
10471
10641
  `);
10472
10642
  if (existsSync19(resolvedFile)) {
10473
- const raw = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10643
+ const raw = JSON.parse(readFileSync14(resolvedFile, "utf8"));
10474
10644
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
10475
10645
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
10476
10646
  writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -10705,7 +10875,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10705
10875
  const hadPreviousFile = existsSync19(resolvedPath);
10706
10876
  if (hadPreviousFile) {
10707
10877
  try {
10708
- previous = JSON.parse(readFileSync12(resolvedPath, "utf8"));
10878
+ previous = JSON.parse(readFileSync14(resolvedPath, "utf8"));
10709
10879
  } catch {
10710
10880
  }
10711
10881
  }
@@ -10793,7 +10963,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10793
10963
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
10794
10964
  const wizard = firstRun;
10795
10965
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
10796
- const routeDir = value("dir") ?? (writeFirstRun ? join18(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
10966
+ const routeDir = value("dir") ?? (writeFirstRun ? join19(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
10797
10967
  const routeBlock = renderConnection(
10798
10968
  found,
10799
10969
  route2,
@@ -10813,6 +10983,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10813
10983
  `);
10814
10984
  }
10815
10985
  if (!wroteConfig) process.stdout.write(routeBlock);
10986
+ if (!flag("no-mcp") && apiKey) {
10987
+ const replace = value("mcp") === "replace";
10988
+ process.stderr.write(`${ensureClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
10989
+ `);
10990
+ }
10816
10991
  if (found.provisioning.length) {
10817
10992
  process.stderr.write(`
10818
10993
  Provisioned for this machine (${ship.name}):
@@ -11203,7 +11378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11203
11378
  break;
11204
11379
  }
11205
11380
  case "install": {
11206
- await installScheduler();
11381
+ if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
11207
11382
  break;
11208
11383
  }
11209
11384
  case "uninstall": {
@@ -11220,6 +11395,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11220
11395
  emit: (m) => process.stdout.write(`${m}
11221
11396
  `),
11222
11397
  warn: (m) => process.stderr.write(`crew uninstall: ${m}
11398
+ `)
11399
+ });
11400
+ }
11401
+ if (flag("all") && host === "macos") {
11402
+ process.stdout.write(`uninstalling every crew launchd unit${dryRun ? " (dry run)" : ""}
11403
+ `);
11404
+ removeAllLaunchdUnits(dryRun, {
11405
+ emit: (m) => process.stdout.write(`${m}
11406
+ `),
11407
+ warn: (m) => process.stderr.write(`crew uninstall: ${m}
11223
11408
  `)
11224
11409
  });
11225
11410
  }
@@ -11231,7 +11416,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11231
11416
  }
11232
11417
  case "log": {
11233
11418
  try {
11234
- const text = readFileSync12(cfg.ship.logFile, "utf8").split("\n");
11419
+ const text = readFileSync14(cfg.ship.logFile, "utf8").split("\n");
11235
11420
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
11236
11421
  `);
11237
11422
  } catch {
@@ -11440,6 +11625,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11440
11625
  if (!agentBinPath || !gitPath) {
11441
11626
  process.stdout.write(
11442
11627
  ` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
11628
+ `
11629
+ );
11630
+ }
11631
+ if (route.apiKey) {
11632
+ const mcpUrl = mcpUrlFor(route.baseUrl);
11633
+ const m = inspectClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrl, key: route.apiKey });
11634
+ process.stdout.write(
11635
+ m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
11636
+ ` : m.state === "no-claude" ? `mcp: claude not found \u2014 Claude Code was not checked
11637
+ ` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
11638
+ ` : `mcp: not registered \u2014 run crew connect
11443
11639
  `
11444
11640
  );
11445
11641
  }
@@ -11475,6 +11671,14 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11475
11671
  process.stdout.write(`attention: ${i.kind} \u2014 ${i.message} (since ${i.since})
11476
11672
  `);
11477
11673
  }
11674
+ if (host === "macos") {
11675
+ const foreign = describeForeignLaunchdUnits(CREW_HOME);
11676
+ if (foreign.length > 0) {
11677
+ process.stdout.write(`foreign launchd units (not this checkout's \u2014 \`crew install\` or \`crew uninstall --all\` clears them):
11678
+ ${foreign.join("\n")}
11679
+ `);
11680
+ }
11681
+ }
11478
11682
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
11479
11683
  if (passengerRoutes.length === 0) {
11480
11684
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.7.1",
3
+ "version": "0.9.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",