@tablation/crew 0.7.0 → 0.8.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 +160 -47
  2. package/package.json +1 -1
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";
@@ -1990,6 +1990,13 @@ function findOnPath(bin, searchPath) {
1990
1990
  }
1991
1991
  return void 0;
1992
1992
  }
1993
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join5(homedir2(), ".docker", "bin")].join(":");
1994
+ function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
1995
+ const schedulerPath = pathFor(ship);
1996
+ if (findOnPath("docker", schedulerPath)) return void 0;
1997
+ const inShell = findOnPath("docker", shellPath);
1998
+ return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname3(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
1999
+ }
1993
2000
  function xmlEscape(s) {
1994
2001
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1995
2002
  }
@@ -5263,8 +5270,7 @@ async function runWizard(p, env) {
5263
5270
  }
5264
5271
  const nodeDir = dirname6(env.nodePath);
5265
5272
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
5266
- const extraPath = deriveExtraPath([nodeDir, pnpmPath ? dirname6(pnpmPath) : void 0]);
5267
- if (extraPath) p.say(`Hooks will get ${extraPath} on their PATH (where node and pnpm live).`);
5273
+ const baseDirs = [nodeDir, pnpmPath ? dirname6(pnpmPath) : void 0];
5268
5274
  p.say(
5269
5275
  `
5270
5276
  Host Passengers lets this machine run the agent sessions that other people's
@@ -5274,6 +5280,11 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
5274
5280
  if (hostPassengers && !env.dockerAvailable) {
5275
5281
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
5276
5282
  }
5283
+ const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
5284
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname6(dockerPath) : void 0]);
5285
+ if (extraPath) {
5286
+ p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
5287
+ }
5277
5288
  return { name, platform: env.platform, agentBin, extraPath, hostPassengers };
5278
5289
  }
5279
5290
  function renderShipBlock(a) {
@@ -7657,6 +7668,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
7657
7668
  );
7658
7669
  }
7659
7670
 
7671
+ // src/claude-mcp.ts
7672
+ import { spawnSync as spawnSync2 } from "child_process";
7673
+ import { readFileSync as readFileSync9 } from "fs";
7674
+ import { homedir as homedir4 } from "os";
7675
+ import { join as join13 } from "path";
7676
+ var MCP_SERVER_NAME = "tablation";
7677
+ var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
7678
+ function mcpUrlFor(baseUrl) {
7679
+ return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
7680
+ }
7681
+ function runClaude(bin, args) {
7682
+ const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
7683
+ const missing = r.error?.code === "ENOENT";
7684
+ return { status: r.status, missing, stderr: r.stderr ?? "" };
7685
+ }
7686
+ function userScopeEntry(home) {
7687
+ try {
7688
+ const j = JSON.parse(readFileSync9(join13(home, ".claude.json"), "utf8"));
7689
+ return j.mcpServers?.[MCP_SERVER_NAME];
7690
+ } catch {
7691
+ return void 0;
7692
+ }
7693
+ }
7694
+ function inspectClaudeMcp(o) {
7695
+ const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
7696
+ if (got.missing) return { state: "no-claude" };
7697
+ if (got.status !== 0) return { state: "missing" };
7698
+ const entry = userScopeEntry(o.home ?? homedir4());
7699
+ if (!entry) {
7700
+ return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
7701
+ }
7702
+ if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
7703
+ const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
7704
+ if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
7705
+ return { state: "registered", url: o.url };
7706
+ }
7707
+ function ensureClaudeMcp(o) {
7708
+ const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
7709
+ const st = inspectClaudeMcp(o);
7710
+ if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
7711
+ if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
7712
+ if (st.state === "different" && !o.replace) {
7713
+ return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
7714
+ }
7715
+ if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
7716
+ if (st.state === "different") {
7717
+ runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
7718
+ }
7719
+ const add = runClaude(o.bin, [
7720
+ "mcp",
7721
+ "add",
7722
+ "--transport",
7723
+ "http",
7724
+ MCP_SERVER_NAME,
7725
+ o.url,
7726
+ "--header",
7727
+ `Authorization: Bearer ${o.key}`,
7728
+ "--scope",
7729
+ "user"
7730
+ ]);
7731
+ if (add.status !== 0) {
7732
+ return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
7733
+ }
7734
+ return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
7735
+ }
7736
+
7660
7737
  // src/doctor-fix.ts
7661
7738
  import { parseDocument as parseDocument2 } from "yaml";
7662
7739
  var sound = (r) => r.hasRepos && r.problems.length === 0;
@@ -7808,13 +7885,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
7808
7885
 
7809
7886
  // src/stream-sweep.ts
7810
7887
  import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
7811
- import { join as join13 } from "path";
7888
+ import { join as join14 } from "path";
7812
7889
  var DAY_MS = 24 * 60 * 60 * 1e3;
7813
7890
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
7814
- const dir = join13(stateDir, "streams");
7891
+ const dir = join14(stateDir, "streams");
7815
7892
  if (!existsSync15(dir)) return [];
7816
7893
  const cutoff = now - retentionDays * DAY_MS;
7817
- return readdirSync2(dir).map((name) => join13(dir, name)).filter((path) => {
7894
+ return readdirSync2(dir).map((name) => join14(dir, name)).filter((path) => {
7818
7895
  try {
7819
7896
  return statSync6(path).mtimeMs < cutoff;
7820
7897
  } catch {
@@ -7841,18 +7918,18 @@ function applyStreamSweep(paths, dryRun, log) {
7841
7918
  }
7842
7919
 
7843
7920
  // src/cli.ts
7844
- import { readFileSync as readFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
7921
+ import { readFileSync as readFileSync13, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
7845
7922
  import { createInterface } from "readline/promises";
7846
7923
  import { dirname as dirOf, resolve as resolvePath } from "path";
7847
7924
 
7848
7925
  // src/ssh-keys.ts
7849
7926
  import { execFileSync as execFileSync3 } from "child_process";
7850
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync9, chmodSync } from "fs";
7851
- import { dirname as dirname8, join as join14 } from "path";
7927
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync10, chmodSync } from "fs";
7928
+ import { dirname as dirname8, join as join15 } from "path";
7852
7929
  var SshKeyError = class extends Error {
7853
7930
  };
7854
7931
  function shipSshKeyPathFor(stateDir) {
7855
- return join14(stateDir, "ssh", "id_ed25519");
7932
+ return join15(stateDir, "ssh", "id_ed25519");
7856
7933
  }
7857
7934
  function ensureShipSshKeypair(stateDir) {
7858
7935
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -7881,7 +7958,7 @@ function ensureShipSshKeypair(stateDir) {
7881
7958
  if (!existsSync16(publicKeyPath)) {
7882
7959
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
7883
7960
  }
7884
- const publicKey = readFileSync9(publicKeyPath, "utf8").trim();
7961
+ const publicKey = readFileSync10(publicKeyPath, "utf8").trim();
7885
7962
  return { privateKeyPath, publicKeyPath, publicKey };
7886
7963
  }
7887
7964
  function sshKeygenAvailable() {
@@ -8095,9 +8172,9 @@ function syncAllPassengerCheckouts(cfg) {
8095
8172
 
8096
8173
  // src/tunnel.ts
8097
8174
  import { spawn as spawn3 } from "child_process";
8098
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
8175
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
8099
8176
  import { randomInt } from "crypto";
8100
- import { join as join15 } from "path";
8177
+ import { join as join16 } from "path";
8101
8178
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
8102
8179
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
8103
8180
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -8164,20 +8241,20 @@ function startTunnel(opts) {
8164
8241
  };
8165
8242
  }
8166
8243
  function tunnelStatePath(stateDir, workspaceId) {
8167
- return join15(stateDir, "passengers", `${workspaceId}.json`);
8244
+ return join16(stateDir, "passengers", `${workspaceId}.json`);
8168
8245
  }
8169
8246
  function readPersistedTunnel(stateDir, workspaceId) {
8170
8247
  const path = tunnelStatePath(stateDir, workspaceId);
8171
8248
  if (!existsSync17(path)) return void 0;
8172
8249
  try {
8173
- return JSON.parse(readFileSync10(path, "utf8"));
8250
+ return JSON.parse(readFileSync11(path, "utf8"));
8174
8251
  } catch {
8175
8252
  return void 0;
8176
8253
  }
8177
8254
  }
8178
8255
  function writePersistedTunnel(stateDir, state) {
8179
8256
  const path = tunnelStatePath(stateDir, state.workspaceId);
8180
- mkdirSync8(join15(stateDir, "passengers"), { recursive: true });
8257
+ mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
8181
8258
  writeFileSync7(path, `${JSON.stringify(state, null, 2)}
8182
8259
  `);
8183
8260
  }
@@ -8275,7 +8352,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
8275
8352
  }
8276
8353
  }
8277
8354
  function readAllPersistedWorkspaceIds(stateDir) {
8278
- const dir = join15(stateDir, "passengers");
8355
+ const dir = join16(stateDir, "passengers");
8279
8356
  if (!existsSync17(dir)) return [];
8280
8357
  return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8281
8358
  }
@@ -8397,8 +8474,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
8397
8474
  import { createServer as createHttpServer } from "http";
8398
8475
  import { spawn as spawn4 } from "child_process";
8399
8476
  import { randomBytes } from "crypto";
8400
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync4 } from "fs";
8401
- import { join as join16 } from "path";
8477
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync4 } from "fs";
8478
+ import { join as join17 } from "path";
8402
8479
 
8403
8480
  // src/secret-compare.ts
8404
8481
  import { timingSafeEqual } from "crypto";
@@ -8487,20 +8564,20 @@ var defaultSpawn2 = (cmd, args, env) => {
8487
8564
  return child;
8488
8565
  };
8489
8566
  function syncDaemonStatePath(stateDir, workspaceId) {
8490
- return join16(stateDir, "passengers", `${workspaceId}-sync.json`);
8567
+ return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
8491
8568
  }
8492
8569
  function readPersistedSyncDaemon(stateDir, workspaceId) {
8493
8570
  const path = syncDaemonStatePath(stateDir, workspaceId);
8494
8571
  if (!existsSync18(path)) return void 0;
8495
8572
  try {
8496
- return JSON.parse(readFileSync11(path, "utf8"));
8573
+ return JSON.parse(readFileSync12(path, "utf8"));
8497
8574
  } catch {
8498
8575
  return void 0;
8499
8576
  }
8500
8577
  }
8501
8578
  function writePersistedSyncDaemon(stateDir, state) {
8502
8579
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
8503
- mkdirSync9(join16(stateDir, "passengers"), { recursive: true });
8580
+ mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
8504
8581
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
8505
8582
  `);
8506
8583
  }
@@ -8521,7 +8598,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
8521
8598
  }
8522
8599
  }
8523
8600
  function readAllPersistedSyncWorkspaceIds(stateDir) {
8524
- const dir = join16(stateDir, "passengers");
8601
+ const dir = join17(stateDir, "passengers");
8525
8602
  if (!existsSync18(dir)) return [];
8526
8603
  const suffix = "-sync.json";
8527
8604
  return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -8580,7 +8657,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8580
8657
 
8581
8658
  // src/daemon.ts
8582
8659
  import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
8583
- import { join as join17 } from "path";
8660
+ import { join as join18 } from "path";
8584
8661
  var DEFAULT_FLOOR_MS = 5e3;
8585
8662
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
8586
8663
  var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
@@ -8619,7 +8696,7 @@ function latestMtime(path) {
8619
8696
  if (!stat.isDirectory()) return 0;
8620
8697
  let latest = stat.mtimeMs;
8621
8698
  for (const entry of readdirSync5(path, { withFileTypes: true })) {
8622
- latest = Math.max(latest, latestMtime(join17(path, entry.name)));
8699
+ latest = Math.max(latest, latestMtime(join18(path, entry.name)));
8623
8700
  }
8624
8701
  return latest;
8625
8702
  }
@@ -8995,7 +9072,7 @@ function usage() {
8995
9072
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
8996
9073
  crew log [route] tail the log
8997
9074
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
8998
- crew connect resolve a workspace's ids into a crew.yaml block
9075
+ 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)
8999
9076
  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)
9000
9077
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
9001
9078
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
@@ -9176,8 +9253,9 @@ if (command === "passenger-sync-daemon") {
9176
9253
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
9177
9254
  if (passengerRoutes.length === 0) return;
9178
9255
  if (!dockerAvailable()) {
9256
+ const pathProblem = dockerPathProblem(cfg.ship, COMMON_DOCKER_DIRS);
9179
9257
  emit.warn(
9180
- "Host Passengers is on for at least one route but Docker is not available \u2014 skipping container sync this cycle",
9258
+ `Host Passengers is on for at least one route but ${pathProblem ?? "Docker is not available"} \u2014 skipping container sync this cycle`,
9181
9259
  { step: "passengers" }
9182
9260
  );
9183
9261
  return;
@@ -9302,7 +9380,12 @@ if (command === "passenger-sync-daemon") {
9302
9380
  process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
9303
9381
  process.exit(2);
9304
9382
  }
9305
- if (!dryRun && cfg.routes.some((r) => r.enabled && r.hostPassengers) && dockerAvailable()) {
9383
+ const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
9384
+ if (installPathProblem) {
9385
+ process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
9386
+ `);
9387
+ process.exitCode = 1;
9388
+ } else if (!dryRun && cfg.routes.some((r) => r.enabled && r.hostPassengers) && dockerAvailable()) {
9306
9389
  const image = passengerImageRef(cfg);
9307
9390
  if (imagePresent(image)) {
9308
9391
  process.stdout.write(`Host Passengers image ${image} is already present
@@ -9333,14 +9416,14 @@ if (command === "passenger-sync-daemon") {
9333
9416
  }
9334
9417
  if (host === "linux" && hasSystemd) {
9335
9418
  const label = labelFor(CREW_HOME, "run");
9336
- const staleTimer = join18(homedir4(), ".config", "systemd", "user", `${label}.timer`);
9419
+ const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
9337
9420
  if (existsSync19(staleTimer)) {
9338
9421
  process.stdout.write(
9339
9422
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
9340
9423
  `
9341
9424
  );
9342
9425
  if (!dryRun) {
9343
- spawnSync2("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9426
+ spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9344
9427
  try {
9345
9428
  unlinkSync5(staleTimer);
9346
9429
  } catch {
@@ -9378,7 +9461,7 @@ if (command === "passenger-sync-daemon") {
9378
9461
  async function doctorFix(o) {
9379
9462
  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))));
9380
9463
  const unitExists = (p) => existsSync19(p);
9381
- const crontab = o.host === "linux" ? spawnSync2("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9464
+ const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9382
9465
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
9383
9466
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
9384
9467
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -9394,7 +9477,7 @@ if (command === "passenger-sync-daemon") {
9394
9477
  for (const a of plan.actions) {
9395
9478
  if (a.kind === "enable") {
9396
9479
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
9397
- writeFileSync9(cfg.configFile, planEnable(readFileSync12(cfg.configFile, "utf8"), a.route));
9480
+ writeFileSync9(cfg.configFile, planEnable(readFileSync13(cfg.configFile, "utf8"), a.route));
9398
9481
  enabledSound.add(a.route);
9399
9482
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
9400
9483
  } else {
@@ -10104,7 +10187,7 @@ ${changed2.map((c) => ` - ${c}
10104
10187
  }
10105
10188
  let failed = false;
10106
10189
  for (const cmd2 of commands) {
10107
- const res2 = spawnSync2(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10190
+ const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10108
10191
  if (res2.stdout) process.stdout.write(res2.stdout);
10109
10192
  if (res2.stderr) process.stderr.write(res2.stderr);
10110
10193
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -10122,7 +10205,7 @@ ${changed2.map((c) => ` - ${c}
10122
10205
  `);
10123
10206
  break;
10124
10207
  }
10125
- const res = spawnSync2(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10208
+ const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
10126
10209
  if (res.stdout) process.stdout.write(res.stdout);
10127
10210
  if (res.stderr) process.stderr.write(res.stderr);
10128
10211
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -10145,7 +10228,7 @@ ${changed2.map((c) => ` - ${c}
10145
10228
  process.exit(1);
10146
10229
  }
10147
10230
  requireArmed("run the daemon loop");
10148
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join18(CREW_HOME, "src"), join18(CREW_HOME, "dist"), join18(CREW_HOME, "bin")];
10231
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join19(CREW_HOME, "src"), join19(CREW_HOME, "dist"), join19(CREW_HOME, "bin")];
10149
10232
  const spawnBreaker = new SpawnBreaker();
10150
10233
  const stopReason = await runDaemonLoop({
10151
10234
  runPass: () => runOnePass({
@@ -10293,7 +10376,7 @@ ${changed2.map((c) => ` - ${c}
10293
10376
  `);
10294
10377
  if (!dryRun) {
10295
10378
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10296
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10379
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10297
10380
  raw.agentPersonas = personaResult.agentPersonas;
10298
10381
  raw.agentSkills = skillResult.agentSkills;
10299
10382
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
@@ -10342,7 +10425,7 @@ ${changed2.map((c) => ` - ${c}
10342
10425
  `);
10343
10426
  if (!dryRun) {
10344
10427
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10345
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10428
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10346
10429
  raw.agentSkills = result.agentSkills;
10347
10430
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
10348
10431
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -10358,14 +10441,14 @@ ${changed2.map((c) => ` - ${c}
10358
10441
  process.exit(2);
10359
10442
  };
10360
10443
  if (sub !== "add" && sub !== "list") usage2();
10361
- const configText = readFileSync12(cfg.configFile, "utf8");
10444
+ const configText = readFileSync13(cfg.configFile, "utf8");
10362
10445
  let routeName = positional[2];
10363
10446
  let addPath = ".";
10364
10447
  let addName;
10365
10448
  if (sub === "list" && !routeName) usage2();
10366
10449
  if (sub === "add") {
10367
10450
  try {
10368
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir4()))));
10451
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
10369
10452
  routeName = a.route;
10370
10453
  addPath = a.path;
10371
10454
  addName = a.name;
@@ -10382,7 +10465,7 @@ ${changed2.map((c) => ` - ${c}
10382
10465
  let resolvedRemotes;
10383
10466
  if (existsSync19(resolvedFile)) {
10384
10467
  try {
10385
- resolvedRemotes = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10468
+ resolvedRemotes = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10386
10469
  } catch {
10387
10470
  }
10388
10471
  }
@@ -10403,7 +10486,7 @@ ${changed2.map((c) => ` - ${c}
10403
10486
  }
10404
10487
  break;
10405
10488
  }
10406
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir4()));
10489
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
10407
10490
  const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
10408
10491
  if (!top) throw new ReposError(`${target} is not a git checkout`);
10409
10492
  const origin = originOf(top);
@@ -10453,7 +10536,7 @@ ${changed2.map((c) => ` - ${c}
10453
10536
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
10454
10537
  `);
10455
10538
  if (existsSync19(resolvedFile)) {
10456
- const raw = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10539
+ const raw = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10457
10540
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
10458
10541
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
10459
10542
  writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -10688,7 +10771,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10688
10771
  const hadPreviousFile = existsSync19(resolvedPath);
10689
10772
  if (hadPreviousFile) {
10690
10773
  try {
10691
- previous = JSON.parse(readFileSync12(resolvedPath, "utf8"));
10774
+ previous = JSON.parse(readFileSync13(resolvedPath, "utf8"));
10692
10775
  } catch {
10693
10776
  }
10694
10777
  }
@@ -10776,7 +10859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10776
10859
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
10777
10860
  const wizard = firstRun;
10778
10861
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
10779
- const routeDir = value("dir") ?? (writeFirstRun ? join18(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
10862
+ const routeDir = value("dir") ?? (writeFirstRun ? join19(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
10780
10863
  const routeBlock = renderConnection(
10781
10864
  found,
10782
10865
  route2,
@@ -10796,6 +10879,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10796
10879
  `);
10797
10880
  }
10798
10881
  if (!wroteConfig) process.stdout.write(routeBlock);
10882
+ if (!flag("no-mcp") && apiKey) {
10883
+ const replace = value("mcp") === "replace";
10884
+ process.stderr.write(`${ensureClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
10885
+ `);
10886
+ }
10799
10887
  if (found.provisioning.length) {
10800
10888
  process.stderr.write(`
10801
10889
  Provisioned for this machine (${ship.name}):
@@ -11214,7 +11302,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11214
11302
  }
11215
11303
  case "log": {
11216
11304
  try {
11217
- const text = readFileSync12(cfg.ship.logFile, "utf8").split("\n");
11305
+ const text = readFileSync13(cfg.ship.logFile, "utf8").split("\n");
11218
11306
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
11219
11307
  `);
11220
11308
  } catch {
@@ -11423,6 +11511,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11423
11511
  if (!agentBinPath || !gitPath) {
11424
11512
  process.stdout.write(
11425
11513
  ` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
11514
+ `
11515
+ );
11516
+ }
11517
+ if (route.apiKey) {
11518
+ const mcpUrl = mcpUrlFor(route.baseUrl);
11519
+ const m = inspectClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrl, key: route.apiKey });
11520
+ process.stdout.write(
11521
+ m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
11522
+ ` : m.state === "no-claude" ? `mcp: claude not found \u2014 Claude Code was not checked
11523
+ ` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
11524
+ ` : `mcp: not registered \u2014 run crew connect
11426
11525
  `
11427
11526
  );
11428
11527
  }
@@ -11463,6 +11562,20 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11463
11562
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
11464
11563
  } else {
11465
11564
  const dockerOk = dockerAvailable();
11565
+ const dockerPathIssue = dockerPathProblem(cfg.ship);
11566
+ const dockerKey = "docker_missing";
11567
+ if (dockerPathIssue) {
11568
+ process.stdout.write(`docker on scheduler PATH: NOT FOUND \u2014 ${dockerPathIssue}
11569
+ `);
11570
+ await raiseShipAttention({ state, emit, route: passengerRoutes[0], ship: cfg.ship }, {
11571
+ kind: "docker_missing",
11572
+ key: dockerKey,
11573
+ message: `Host Passengers is on but ${dockerPathIssue}`,
11574
+ since: (/* @__PURE__ */ new Date()).toISOString()
11575
+ });
11576
+ } else {
11577
+ clearShipAttention(state, dockerKey);
11578
+ }
11466
11579
  const keygenOk = sshKeygenAvailable();
11467
11580
  const image = passengerImageRef(cfg);
11468
11581
  const imageOk = dockerOk && imagePresent(image);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",