@tablation/crew 0.31.0 → 0.33.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 +171 -22
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { resolve as resolve4, dirname as dirname12, join as join21, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir6 } from "os";
7
- import { spawnSync as spawnSync3 } from "child_process";
7
+ import { spawnSync as spawnSync4 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
10
  import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
@@ -159,6 +159,7 @@ var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
159
159
 
160
160
  // src/pair-install.ts
161
161
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
162
+ import { spawnSync as spawnSync2 } from "child_process";
162
163
  import { homedir as homedir2 } from "os";
163
164
  import { dirname as dirname2, join as join3 } from "path";
164
165
 
@@ -385,7 +386,7 @@ var claudeAdapter = {
385
386
  })] : [],
386
387
  ...spec.args
387
388
  ];
388
- return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
389
+ return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
389
390
  },
390
391
  runEnv(spec) {
391
392
  return {
@@ -668,10 +669,144 @@ var geminiPair = {
668
669
  return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
669
670
  }
670
671
  };
672
+ var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
673
+ function vscodeUserDirs(o) {
674
+ const home = o.home ?? homedir2();
675
+ const platform2 = o.platform ?? process.platform;
676
+ const root = platform2 === "darwin" ? join3(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join3(home, "AppData", "Roaming") : join3(home, ".config");
677
+ const all = VSCODE_FLAVOURS.map((f) => join3(root, f, "User"));
678
+ const present = all.filter((d) => existsSync2(d));
679
+ return present.length > 0 ? present : [all[0]];
680
+ }
681
+ var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
682
+ function readVscodeMcp(path) {
683
+ const text = readIfExists(path);
684
+ if (text === void 0 || text.trim() === "") return {};
685
+ try {
686
+ const j = JSON.parse(text);
687
+ return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
688
+ } catch {
689
+ return void 0;
690
+ }
691
+ }
692
+ var COPILOT_AUTH = `Bearer \${env:${PAIR_KEY_VAR}}`;
693
+ var copilotEntry = (url) => ({ type: "http", url, headers: { Authorization: COPILOT_AUTH } });
694
+ function copilotMcpState(s, url) {
695
+ const e = s.servers?.[SERVER];
696
+ if (!e) return { state: "missing" };
697
+ if (e.url !== url) return { state: "different", detail: `it points at ${e.url ?? "(no url)"}, not ${url}` };
698
+ if (e.headers?.Authorization !== COPILOT_AUTH) return { state: "different", detail: `it does not read its key from \${env:${PAIR_KEY_VAR}}` };
699
+ return { state: "registered" };
700
+ }
701
+ function installCopilotInstructions(path, dryRun) {
702
+ const before = readIfExists(path);
703
+ const base = before ?? COPILOT_FRONTMATTER;
704
+ if (pairBlockIsBroken(base)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
705
+ const after = upsertPairBlock(base, pairBlock("copilot"));
706
+ if (after === before) return { changed: false };
707
+ if (!dryRun) writeFileMkdir(path, after);
708
+ return { changed: true };
709
+ }
710
+ function removeCopilotInstructions(path, dryRun) {
711
+ const before = readIfExists(path);
712
+ if (before === void 0 || !hasPairBlock(before)) return false;
713
+ if (dryRun) return true;
714
+ const after = removePairBlock(before);
715
+ if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync2(path);
716
+ else writeFileSync(path, after);
717
+ return true;
718
+ }
719
+ var copilotPair = {
720
+ harness: "copilot",
721
+ displayName: "GitHub Copilot (VS Code)",
722
+ installPair(o) {
723
+ const lines = [];
724
+ for (const dir of vscodeUserDirs(o)) {
725
+ const mcpPath = join3(dir, "mcp.json");
726
+ const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
727
+ const settings = readVscodeMcp(mcpPath);
728
+ if (!settings) {
729
+ lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
730
+ continue;
731
+ }
732
+ const st = copilotMcpState(settings, o.url);
733
+ if (st.state === "different" && !o.replace) {
734
+ lines.push(`pair(copilot): left ${mcpPath} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`);
735
+ continue;
736
+ }
737
+ const instr = installCopilotInstructions(instrPath, o.dryRun);
738
+ if (instr.error) {
739
+ lines.push(`pair(copilot): ${instr.error}`);
740
+ continue;
741
+ }
742
+ const mcpChanged = st.state !== "registered";
743
+ if (mcpChanged && !o.dryRun) {
744
+ const next = { ...settings, servers: { ...settings.servers, [SERVER]: copilotEntry(o.url) } };
745
+ writeFileMkdir(mcpPath, `${JSON.stringify(next, null, 2)}
746
+ `);
747
+ }
748
+ lines.push(!instr.changed && !mcpChanged ? `pair(copilot): already installed (${instrPath}, ${mcpPath})` : `pair(copilot): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair instructions in ${instrPath} and ${SERVER} in ${mcpPath} (key read from \${env:${PAIR_KEY_VAR}})`);
749
+ }
750
+ return lines.join("\n");
751
+ },
752
+ verifyPair(o) {
753
+ const verdicts = vscodeUserDirs(o).map((dir) => {
754
+ const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
755
+ const mcpPath = join3(dir, "mcp.json");
756
+ const b = blockVerdict(instrPath, "copilot");
757
+ if (!b.ok) return b;
758
+ const settings = readVscodeMcp(mcpPath);
759
+ if (!settings) return { ok: false, detail: `${mcpPath} is not a plain JSON object` };
760
+ const st = copilotMcpState(settings, o.url);
761
+ if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${mcpPath}` };
762
+ if (st.state === "different") return { ok: false, detail: `${SERVER} in ${mcpPath} ${st.detail}` };
763
+ return { ok: true, detail: `${instrPath}, ${mcpPath}` };
764
+ });
765
+ return verdicts.find((v) => !v.ok) ?? { ok: true, detail: verdicts.map((v) => v.detail).join("; ") };
766
+ },
767
+ removePair(o) {
768
+ const lines = [];
769
+ for (const dir of vscodeUserDirs(o)) {
770
+ const mcpPath = join3(dir, "mcp.json");
771
+ const hadBlock = removeCopilotInstructions(join3(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
772
+ const settings = readVscodeMcp(mcpPath);
773
+ const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
774
+ const hadMcp = st.state === "registered";
775
+ const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
776
+ if (hadMcp && settings && !o.dryRun) {
777
+ const { [SERVER]: _gone, ...rest } = settings.servers ?? {};
778
+ const next = { ...settings };
779
+ if (Object.keys(rest).length > 0) next.servers = rest;
780
+ else delete next.servers;
781
+ if (Object.keys(next).length === 0) rmSync2(mcpPath);
782
+ else writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}
783
+ `);
784
+ }
785
+ const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
786
+ lines.push(hadBlock || hadMcp ? `pair(copilot): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what} (${dir})${kept}` : `pair(copilot): nothing to remove in ${dir}${kept}`);
787
+ }
788
+ return lines.join("\n");
789
+ },
790
+ /**
791
+ * VS Code launched from the Dock or Spotlight does not inherit a shell's
792
+ * exports, so `${env:CREW_API_KEY}` expands to nothing there. macOS only: the
793
+ * login environment is `launchctl`'s, and it is the one place we can ask.
794
+ */
795
+ envWarning(o) {
796
+ if ((o.platform ?? process.platform) !== "darwin") return void 0;
797
+ const get2 = o.loginEnv ?? ((name) => {
798
+ const r = spawnSync2("launchctl", ["getenv", name], { encoding: "utf8" });
799
+ return r.status === 0 ? r.stdout.trim() : void 0;
800
+ });
801
+ if (get2(PAIR_KEY_VAR)) return void 0;
802
+ return `$${PAIR_KEY_VAR} is not in the login environment, so VS Code started from the Dock will not send the tracker key. Launch VS Code from a terminal (\`code .\`), or run \`launchctl setenv ${PAIR_KEY_VAR} <key>\` once per login.`;
803
+ }
804
+ };
671
805
  var PAIR_INSTALLERS = {
672
806
  [claudePair.harness]: claudePair,
673
807
  [codexPair.harness]: codexPair,
674
- [geminiPair.harness]: geminiPair
808
+ [geminiPair.harness]: geminiPair,
809
+ [copilotPair.harness]: copilotPair
675
810
  };
676
811
  var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
677
812
  var DEFAULT_PAIR = ["claude"];
@@ -730,9 +865,15 @@ function resolveToolchain(ship, route) {
730
865
  return found;
731
866
  }
732
867
  function toolchainVendor(spec) {
733
- if (spec.launcher && basename(spec.launcher) === "ori") return "openrouter";
868
+ if (isOriLauncher(spec)) return "openrouter";
734
869
  return adapterFor(spec.harness).vendor;
735
870
  }
871
+ function isOriLauncher(spec) {
872
+ return !!spec.launcher && basename(spec.launcher) === "ori";
873
+ }
874
+ function toolchainProvider(spec) {
875
+ return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
876
+ }
736
877
  function resolveSeat(ship, route, role, persona = {}) {
737
878
  const warnings = [];
738
879
  const base = resolveToolchain(ship, route);
@@ -1774,7 +1915,7 @@ Create the first of those:
1774
1915
  function expandCommand(cmd, base) {
1775
1916
  return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
1776
1917
  }
1777
- var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1918
+ var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens"]);
1778
1919
  function parsePair(raw, missing) {
1779
1920
  if (raw === void 0) return void 0;
1780
1921
  if (!Array.isArray(raw)) {
@@ -1836,6 +1977,7 @@ function parseToolchains(raw, base, missing, file) {
1836
1977
  harness,
1837
1978
  bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
1838
1979
  ...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
1980
+ ...entryRaw.launcher && entryRaw.launcherBin ? { launcherBin: expandCommand(String(entryRaw.launcherBin), base) } : {},
1839
1981
  model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
1840
1982
  tiers,
1841
1983
  args: Array.isArray(args) ? args.map(String) : [],
@@ -2901,7 +3043,7 @@ function describeRelease(o, route) {
2901
3043
  import { dirname as dirname4, join as join7, extname } from "path";
2902
3044
  import { homedir as homedir4 } from "os";
2903
3045
  import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
2904
- import { spawnSync as spawnSync2 } from "child_process";
3046
+ import { spawnSync as spawnSync3 } from "child_process";
2905
3047
  import { createHash } from "crypto";
2906
3048
 
2907
3049
  // src/runtime-info.ts
@@ -3192,7 +3334,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
3192
3334
  }
3193
3335
  function runAll(commands, cwd, log) {
3194
3336
  for (const [bin, ...args] of commands) {
3195
- const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
3337
+ const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
3196
3338
  if (res.error || res.status !== 0) {
3197
3339
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
3198
3340
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -3257,7 +3399,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3257
3399
  if (dryRun) {
3258
3400
  log.emit(`would run: launchctl remove ${plan.removeLabel}`);
3259
3401
  } else {
3260
- if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3402
+ if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3261
3403
  log.emit(`removed loaded job ${plan.removeLabel}`);
3262
3404
  }
3263
3405
  }
@@ -3294,7 +3436,7 @@ function programFromPlist(text) {
3294
3436
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
3295
3437
  }
3296
3438
  function loadedCrewLabels() {
3297
- const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
3439
+ const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3298
3440
  if (list.status !== 0) return [];
3299
3441
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3300
3442
  }
@@ -3330,8 +3472,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
3330
3472
  log.emit(`would unload and remove ${unit.label}`);
3331
3473
  return;
3332
3474
  }
3333
- if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3334
- spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
3475
+ if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3476
+ spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
3335
3477
  if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
3336
3478
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3337
3479
  }
@@ -3364,7 +3506,7 @@ function parseLaunchctlList(text) {
3364
3506
  return jobs;
3365
3507
  }
3366
3508
  function describeForeignLaunchdUnits(crewHome, dir) {
3367
- const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
3509
+ const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3368
3510
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
3369
3511
  const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
3370
3512
  return foreign.map((u) => {
@@ -3377,11 +3519,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
3377
3519
  });
3378
3520
  }
3379
3521
  function readCrontab() {
3380
- const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
3522
+ const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
3381
3523
  return res.status === 0 ? res.stdout ?? "" : "";
3382
3524
  }
3383
3525
  function writeCrontab(text) {
3384
- spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
3526
+ spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
3385
3527
  }
3386
3528
  function addCrontabLine(marker, line) {
3387
3529
  const existing = readCrontab();
@@ -3405,7 +3547,7 @@ function removeCrontabLine(marker) {
3405
3547
  }
3406
3548
  function detectSystemd() {
3407
3549
  if (process.platform !== "linux") return false;
3408
- const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
3550
+ const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
3409
3551
  return !res.error;
3410
3552
  }
3411
3553
 
@@ -6627,7 +6769,7 @@ function planAgentRun(o) {
6627
6769
  args: invocation.args,
6628
6770
  toolchain: toolchain.name,
6629
6771
  harness: toolchain.harness,
6630
- provider: adapter.provider(toolchain),
6772
+ provider: toolchainProvider(toolchain),
6631
6773
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
6632
6774
  warnings: seat.warnings,
6633
6775
  prompt,
@@ -11727,7 +11869,7 @@ if (command === "passenger-sync-daemon") {
11727
11869
  `
11728
11870
  );
11729
11871
  if (!dryRun) {
11730
- spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
11872
+ spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
11731
11873
  try {
11732
11874
  unlinkSync5(staleTimer);
11733
11875
  } catch {
@@ -11766,7 +11908,7 @@ if (command === "passenger-sync-daemon") {
11766
11908
  async function doctorFix(o) {
11767
11909
  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))));
11768
11910
  const unitExists = (p) => existsSync21(p);
11769
- const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
11911
+ const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
11770
11912
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
11771
11913
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
11772
11914
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -12638,7 +12780,7 @@ ${changed2.map((c) => ` - ${c}
12638
12780
  }
12639
12781
  let failed = false;
12640
12782
  for (const cmd2 of commands) {
12641
- const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12783
+ const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12642
12784
  if (res2.stdout) process.stdout.write(res2.stdout);
12643
12785
  if (res2.stderr) process.stderr.write(res2.stderr);
12644
12786
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -12656,7 +12798,7 @@ ${changed2.map((c) => ` - ${c}
12656
12798
  `);
12657
12799
  break;
12658
12800
  }
12659
- const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12801
+ const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12660
12802
  if (res.stdout) process.stdout.write(res.stdout);
12661
12803
  if (res.stderr) process.stderr.write(res.stderr);
12662
12804
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -14189,15 +14331,19 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14189
14331
  let allBinsFound = true;
14190
14332
  for (const tc of shipToolchains(cfg.ship)) {
14191
14333
  const adapter = adapterFor(tc.harness);
14192
- const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
14334
+ const binPath = findOnPath(tc.launcherBin ?? tc.launcher ?? tc.bin, schedulerPath);
14193
14335
  if (!binPath) allBinsFound = false;
14194
14336
  const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
14195
14337
  process.stdout.write(
14196
- `${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
14338
+ `${label} ${tc.launcherBin ?? tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
14197
14339
  `
14198
14340
  );
14199
14341
  for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
14200
14342
  `);
14343
+ if (isOriLauncher(tc)) {
14344
+ process.stdout.write(` ori: headless auth under the scheduler is unverified \u2014 run \`ori login\` as the scheduler user, then confirm a run records provider openrouter (see docs/toolchains.md)
14345
+ `);
14346
+ }
14201
14347
  }
14202
14348
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
14203
14349
  `);
@@ -14231,6 +14377,9 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14231
14377
  process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
14232
14378
  ` : ` NOT installed \u2014 ${v.detail}; run crew connect
14233
14379
  `));
14380
+ const envWarn = inst.envWarning?.({ url: mcpUrlFor(route.baseUrl) });
14381
+ if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
14382
+ `);
14234
14383
  }
14235
14384
  const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
14236
14385
  if (route.apiKey && route.resolved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",