@tablation/crew 0.7.1 → 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 +125 -43
  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";
@@ -7668,6 +7668,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
7668
7668
  );
7669
7669
  }
7670
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
+
7671
7737
  // src/doctor-fix.ts
7672
7738
  import { parseDocument as parseDocument2 } from "yaml";
7673
7739
  var sound = (r) => r.hasRepos && r.problems.length === 0;
@@ -7819,13 +7885,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
7819
7885
 
7820
7886
  // src/stream-sweep.ts
7821
7887
  import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
7822
- import { join as join13 } from "path";
7888
+ import { join as join14 } from "path";
7823
7889
  var DAY_MS = 24 * 60 * 60 * 1e3;
7824
7890
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
7825
- const dir = join13(stateDir, "streams");
7891
+ const dir = join14(stateDir, "streams");
7826
7892
  if (!existsSync15(dir)) return [];
7827
7893
  const cutoff = now - retentionDays * DAY_MS;
7828
- return readdirSync2(dir).map((name) => join13(dir, name)).filter((path) => {
7894
+ return readdirSync2(dir).map((name) => join14(dir, name)).filter((path) => {
7829
7895
  try {
7830
7896
  return statSync6(path).mtimeMs < cutoff;
7831
7897
  } catch {
@@ -7852,18 +7918,18 @@ function applyStreamSweep(paths, dryRun, log) {
7852
7918
  }
7853
7919
 
7854
7920
  // src/cli.ts
7855
- 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";
7856
7922
  import { createInterface } from "readline/promises";
7857
7923
  import { dirname as dirOf, resolve as resolvePath } from "path";
7858
7924
 
7859
7925
  // src/ssh-keys.ts
7860
7926
  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";
7927
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync10, chmodSync } from "fs";
7928
+ import { dirname as dirname8, join as join15 } from "path";
7863
7929
  var SshKeyError = class extends Error {
7864
7930
  };
7865
7931
  function shipSshKeyPathFor(stateDir) {
7866
- return join14(stateDir, "ssh", "id_ed25519");
7932
+ return join15(stateDir, "ssh", "id_ed25519");
7867
7933
  }
7868
7934
  function ensureShipSshKeypair(stateDir) {
7869
7935
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -7892,7 +7958,7 @@ function ensureShipSshKeypair(stateDir) {
7892
7958
  if (!existsSync16(publicKeyPath)) {
7893
7959
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
7894
7960
  }
7895
- const publicKey = readFileSync9(publicKeyPath, "utf8").trim();
7961
+ const publicKey = readFileSync10(publicKeyPath, "utf8").trim();
7896
7962
  return { privateKeyPath, publicKeyPath, publicKey };
7897
7963
  }
7898
7964
  function sshKeygenAvailable() {
@@ -8106,9 +8172,9 @@ function syncAllPassengerCheckouts(cfg) {
8106
8172
 
8107
8173
  // src/tunnel.ts
8108
8174
  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";
8175
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
8110
8176
  import { randomInt } from "crypto";
8111
- import { join as join15 } from "path";
8177
+ import { join as join16 } from "path";
8112
8178
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
8113
8179
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
8114
8180
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -8175,20 +8241,20 @@ function startTunnel(opts) {
8175
8241
  };
8176
8242
  }
8177
8243
  function tunnelStatePath(stateDir, workspaceId) {
8178
- return join15(stateDir, "passengers", `${workspaceId}.json`);
8244
+ return join16(stateDir, "passengers", `${workspaceId}.json`);
8179
8245
  }
8180
8246
  function readPersistedTunnel(stateDir, workspaceId) {
8181
8247
  const path = tunnelStatePath(stateDir, workspaceId);
8182
8248
  if (!existsSync17(path)) return void 0;
8183
8249
  try {
8184
- return JSON.parse(readFileSync10(path, "utf8"));
8250
+ return JSON.parse(readFileSync11(path, "utf8"));
8185
8251
  } catch {
8186
8252
  return void 0;
8187
8253
  }
8188
8254
  }
8189
8255
  function writePersistedTunnel(stateDir, state) {
8190
8256
  const path = tunnelStatePath(stateDir, state.workspaceId);
8191
- mkdirSync8(join15(stateDir, "passengers"), { recursive: true });
8257
+ mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
8192
8258
  writeFileSync7(path, `${JSON.stringify(state, null, 2)}
8193
8259
  `);
8194
8260
  }
@@ -8286,7 +8352,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
8286
8352
  }
8287
8353
  }
8288
8354
  function readAllPersistedWorkspaceIds(stateDir) {
8289
- const dir = join15(stateDir, "passengers");
8355
+ const dir = join16(stateDir, "passengers");
8290
8356
  if (!existsSync17(dir)) return [];
8291
8357
  return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8292
8358
  }
@@ -8408,8 +8474,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
8408
8474
  import { createServer as createHttpServer } from "http";
8409
8475
  import { spawn as spawn4 } from "child_process";
8410
8476
  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";
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";
8413
8479
 
8414
8480
  // src/secret-compare.ts
8415
8481
  import { timingSafeEqual } from "crypto";
@@ -8498,20 +8564,20 @@ var defaultSpawn2 = (cmd, args, env) => {
8498
8564
  return child;
8499
8565
  };
8500
8566
  function syncDaemonStatePath(stateDir, workspaceId) {
8501
- return join16(stateDir, "passengers", `${workspaceId}-sync.json`);
8567
+ return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
8502
8568
  }
8503
8569
  function readPersistedSyncDaemon(stateDir, workspaceId) {
8504
8570
  const path = syncDaemonStatePath(stateDir, workspaceId);
8505
8571
  if (!existsSync18(path)) return void 0;
8506
8572
  try {
8507
- return JSON.parse(readFileSync11(path, "utf8"));
8573
+ return JSON.parse(readFileSync12(path, "utf8"));
8508
8574
  } catch {
8509
8575
  return void 0;
8510
8576
  }
8511
8577
  }
8512
8578
  function writePersistedSyncDaemon(stateDir, state) {
8513
8579
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
8514
- mkdirSync9(join16(stateDir, "passengers"), { recursive: true });
8580
+ mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
8515
8581
  writeFileSync8(path, `${JSON.stringify(state, null, 2)}
8516
8582
  `);
8517
8583
  }
@@ -8532,7 +8598,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
8532
8598
  }
8533
8599
  }
8534
8600
  function readAllPersistedSyncWorkspaceIds(stateDir) {
8535
- const dir = join16(stateDir, "passengers");
8601
+ const dir = join17(stateDir, "passengers");
8536
8602
  if (!existsSync18(dir)) return [];
8537
8603
  const suffix = "-sync.json";
8538
8604
  return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -8591,7 +8657,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8591
8657
 
8592
8658
  // src/daemon.ts
8593
8659
  import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
8594
- import { join as join17 } from "path";
8660
+ import { join as join18 } from "path";
8595
8661
  var DEFAULT_FLOOR_MS = 5e3;
8596
8662
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
8597
8663
  var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
@@ -8630,7 +8696,7 @@ function latestMtime(path) {
8630
8696
  if (!stat.isDirectory()) return 0;
8631
8697
  let latest = stat.mtimeMs;
8632
8698
  for (const entry of readdirSync5(path, { withFileTypes: true })) {
8633
- latest = Math.max(latest, latestMtime(join17(path, entry.name)));
8699
+ latest = Math.max(latest, latestMtime(join18(path, entry.name)));
8634
8700
  }
8635
8701
  return latest;
8636
8702
  }
@@ -9006,7 +9072,7 @@ function usage() {
9006
9072
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
9007
9073
  crew log [route] tail the log
9008
9074
  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
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)
9010
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)
9011
9077
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
9012
9078
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
@@ -9350,14 +9416,14 @@ if (command === "passenger-sync-daemon") {
9350
9416
  }
9351
9417
  if (host === "linux" && hasSystemd) {
9352
9418
  const label = labelFor(CREW_HOME, "run");
9353
- const staleTimer = join18(homedir4(), ".config", "systemd", "user", `${label}.timer`);
9419
+ const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
9354
9420
  if (existsSync19(staleTimer)) {
9355
9421
  process.stdout.write(
9356
9422
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
9357
9423
  `
9358
9424
  );
9359
9425
  if (!dryRun) {
9360
- spawnSync2("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9426
+ spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
9361
9427
  try {
9362
9428
  unlinkSync5(staleTimer);
9363
9429
  } catch {
@@ -9395,7 +9461,7 @@ if (command === "passenger-sync-daemon") {
9395
9461
  async function doctorFix(o) {
9396
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))));
9397
9463
  const unitExists = (p) => existsSync19(p);
9398
- const crontab = o.host === "linux" ? spawnSync2("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9464
+ const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
9399
9465
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
9400
9466
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
9401
9467
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -9411,7 +9477,7 @@ if (command === "passenger-sync-daemon") {
9411
9477
  for (const a of plan.actions) {
9412
9478
  if (a.kind === "enable") {
9413
9479
  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));
9480
+ writeFileSync9(cfg.configFile, planEnable(readFileSync13(cfg.configFile, "utf8"), a.route));
9415
9481
  enabledSound.add(a.route);
9416
9482
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
9417
9483
  } else {
@@ -10121,7 +10187,7 @@ ${changed2.map((c) => ` - ${c}
10121
10187
  }
10122
10188
  let failed = false;
10123
10189
  for (const cmd2 of commands) {
10124
- 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" });
10125
10191
  if (res2.stdout) process.stdout.write(res2.stdout);
10126
10192
  if (res2.stderr) process.stderr.write(res2.stderr);
10127
10193
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -10139,7 +10205,7 @@ ${changed2.map((c) => ` - ${c}
10139
10205
  `);
10140
10206
  break;
10141
10207
  }
10142
- 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" });
10143
10209
  if (res.stdout) process.stdout.write(res.stdout);
10144
10210
  if (res.stderr) process.stderr.write(res.stderr);
10145
10211
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -10162,7 +10228,7 @@ ${changed2.map((c) => ` - ${c}
10162
10228
  process.exit(1);
10163
10229
  }
10164
10230
  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")];
10231
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join19(CREW_HOME, "src"), join19(CREW_HOME, "dist"), join19(CREW_HOME, "bin")];
10166
10232
  const spawnBreaker = new SpawnBreaker();
10167
10233
  const stopReason = await runDaemonLoop({
10168
10234
  runPass: () => runOnePass({
@@ -10310,7 +10376,7 @@ ${changed2.map((c) => ` - ${c}
10310
10376
  `);
10311
10377
  if (!dryRun) {
10312
10378
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10313
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10379
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10314
10380
  raw.agentPersonas = personaResult.agentPersonas;
10315
10381
  raw.agentSkills = skillResult.agentSkills;
10316
10382
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
@@ -10359,7 +10425,7 @@ ${changed2.map((c) => ` - ${c}
10359
10425
  `);
10360
10426
  if (!dryRun) {
10361
10427
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10362
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync12(resolvedPath, "utf8")) : {};
10428
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10363
10429
  raw.agentSkills = result.agentSkills;
10364
10430
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
10365
10431
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -10375,14 +10441,14 @@ ${changed2.map((c) => ` - ${c}
10375
10441
  process.exit(2);
10376
10442
  };
10377
10443
  if (sub !== "add" && sub !== "list") usage2();
10378
- const configText = readFileSync12(cfg.configFile, "utf8");
10444
+ const configText = readFileSync13(cfg.configFile, "utf8");
10379
10445
  let routeName = positional[2];
10380
10446
  let addPath = ".";
10381
10447
  let addName;
10382
10448
  if (sub === "list" && !routeName) usage2();
10383
10449
  if (sub === "add") {
10384
10450
  try {
10385
- 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()))));
10386
10452
  routeName = a.route;
10387
10453
  addPath = a.path;
10388
10454
  addName = a.name;
@@ -10399,7 +10465,7 @@ ${changed2.map((c) => ` - ${c}
10399
10465
  let resolvedRemotes;
10400
10466
  if (existsSync19(resolvedFile)) {
10401
10467
  try {
10402
- resolvedRemotes = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10468
+ resolvedRemotes = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10403
10469
  } catch {
10404
10470
  }
10405
10471
  }
@@ -10420,7 +10486,7 @@ ${changed2.map((c) => ` - ${c}
10420
10486
  }
10421
10487
  break;
10422
10488
  }
10423
- const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir4()));
10489
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
10424
10490
  const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
10425
10491
  if (!top) throw new ReposError(`${target} is not a git checkout`);
10426
10492
  const origin = originOf(top);
@@ -10470,7 +10536,7 @@ ${changed2.map((c) => ` - ${c}
10470
10536
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
10471
10537
  `);
10472
10538
  if (existsSync19(resolvedFile)) {
10473
- const raw = JSON.parse(readFileSync12(resolvedFile, "utf8"));
10539
+ const raw = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10474
10540
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
10475
10541
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
10476
10542
  writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -10705,7 +10771,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10705
10771
  const hadPreviousFile = existsSync19(resolvedPath);
10706
10772
  if (hadPreviousFile) {
10707
10773
  try {
10708
- previous = JSON.parse(readFileSync12(resolvedPath, "utf8"));
10774
+ previous = JSON.parse(readFileSync13(resolvedPath, "utf8"));
10709
10775
  } catch {
10710
10776
  }
10711
10777
  }
@@ -10793,7 +10859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10793
10859
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
10794
10860
  const wizard = firstRun;
10795
10861
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
10796
- 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);
10797
10863
  const routeBlock = renderConnection(
10798
10864
  found,
10799
10865
  route2,
@@ -10813,6 +10879,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10813
10879
  `);
10814
10880
  }
10815
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
+ }
10816
10887
  if (found.provisioning.length) {
10817
10888
  process.stderr.write(`
10818
10889
  Provisioned for this machine (${ship.name}):
@@ -11231,7 +11302,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11231
11302
  }
11232
11303
  case "log": {
11233
11304
  try {
11234
- const text = readFileSync12(cfg.ship.logFile, "utf8").split("\n");
11305
+ const text = readFileSync13(cfg.ship.logFile, "utf8").split("\n");
11235
11306
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
11236
11307
  `);
11237
11308
  } catch {
@@ -11440,6 +11511,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11440
11511
  if (!agentBinPath || !gitPath) {
11441
11512
  process.stdout.write(
11442
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
11443
11525
  `
11444
11526
  );
11445
11527
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.7.1",
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",