premanmcp 0.10.2 → 0.10.3

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 +5 -3
  2. package/bin/connect.js +115 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,9 +58,11 @@ to go restart anything, in cheapest-first order:
58
58
  A self-test that answers from an unexpected backend is reported with the file that
59
59
  redirected it — a repo-local `preman-mcp.config.json` with `"PREMAN_CONFIG_OVERRIDE": true`
60
60
  wins over the MCP config env, and otherwise only fills in what the env leaves unset. When
61
- that file overrides `PREMAN_BACKEND`, `connect` stops there rather than waiting: an agent
62
- started in that directory reads the same file and checks in somewhere else, so it names the
63
- file and tells you how to connect to either backend.
61
+ that file overrides `PREMAN_BACKEND`, the config `connect` wrote is still correct: the
62
+ server reads that file only from the directory it starts in, so `connect` names the file
63
+ and then retries from your home directory, where the override cannot reach it — self-test
64
+ first, then your agent. Agents you start in the overriding directory keep using its
65
+ backend, which is the point of the file.
64
66
 
65
67
  Once linked, `connect` finishes onboarding without handing you homework:
66
68
 
package/bin/connect.js CHANGED
@@ -690,6 +690,11 @@ export function openInNewTerminal(command, options = {}) {
690
690
  * machine with no terminal emulator — does this fall back to the headless run,
691
691
  * which still finishes the link even though nobody sees it happen.
692
692
  *
693
+ * `cwd` is where the agent starts, and it is load-bearing rather than tidiness:
694
+ * the MCP server reads its repo config out of the working directory, so starting
695
+ * the agent elsewhere is how a caller escapes a directory that would otherwise
696
+ * redirect it to a backend this key was never issued for.
697
+ *
693
698
  * Returns `ran: false` when the agent's binary is absent or will not start, and
694
699
  * the caller falls back to the printed instructions.
695
700
  */
@@ -706,13 +711,14 @@ export async function autoCheckIn(
706
711
  ),
707
712
  serverName = "preman",
708
713
  intervalMs = Number(process.env.PREMAN_CONNECT_POLL_MS) || 3000,
714
+ cwd = process.cwd(),
709
715
  onLaunch = () => {},
710
716
  onPoll = null,
711
717
  } = {}
712
718
  ) {
713
719
  const session = interactiveCheckIn(agent, serverName);
714
720
  if (session && onPath(session.bin)) {
715
- const { opened, terminal } = openInNewTerminal(commandLine(session), { cwd: process.cwd() });
721
+ const { opened, terminal } = openInNewTerminal(commandLine(session), { cwd });
716
722
  if (opened) {
717
723
  onLaunch({ mode: "interactive", bin: session.bin, terminal });
718
724
  // Detached, so there is no exit to watch for and no output to quote: the
@@ -732,7 +738,7 @@ export async function autoCheckIn(
732
738
  // Piped rather than ignored: an agent that runs and does not check in used to
733
739
  // report exactly that and nothing else, which is the least useful sentence
734
740
  // available. Its own last words usually name the cause.
735
- child = spawn(spec.bin, spec.args, { stdio: ["ignore", "pipe", "pipe"] });
741
+ child = spawn(spec.bin, spec.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
736
742
  } catch (error) {
737
743
  return { ran: false, connected: false, reason: error.message };
738
744
  }
@@ -1655,7 +1661,14 @@ export async function connectCommand(commandArgs) {
1655
1661
  return;
1656
1662
  }
1657
1663
 
1658
- if (!(await establishCheckIn(args, agent, apiKey, { serverName, written, serverConfig }))) {
1664
+ if (
1665
+ !(await establishCheckIn(args, agent, apiKey, {
1666
+ serverName,
1667
+ written,
1668
+ serverConfig,
1669
+ projectInstall,
1670
+ }))
1671
+ ) {
1659
1672
  // Still honour an explicitly-passed credential, but do not open a new prompt
1660
1673
  // on top of a connect that just told the user something went wrong.
1661
1674
  await captureDispatchCredential(args, agent, apiKey, { prompt: false });
@@ -1687,6 +1700,30 @@ function backendRedirect(repo, status, serverConfig) {
1687
1700
  return { path: repo.path, actual, wanted };
1688
1701
  }
1689
1702
 
1703
+ /**
1704
+ * Somewhere to start the agent when this directory would redirect its backend.
1705
+ *
1706
+ * The server reads its repo config from the working directory and nowhere else —
1707
+ * no walk up to the parents — so any directory without one of those two files is
1708
+ * already out of the override's reach. Home first, because an agent started there
1709
+ * is in a place the user recognises; a temp dir only if home is itself a repo
1710
+ * carrying a config.
1711
+ *
1712
+ * Null means every candidate was covered, and the caller has nothing to offer but
1713
+ * the instructions.
1714
+ */
1715
+ export function neutralCwd(candidates = [os.homedir(), os.tmpdir()]) {
1716
+ return (
1717
+ candidates.find(
1718
+ (dir) =>
1719
+ dir &&
1720
+ existsSync(dir) &&
1721
+ !existsSync(path.join(dir, ".cursor", "preman-mcp.config.json")) &&
1722
+ !existsSync(path.join(dir, "preman-mcp.config.json"))
1723
+ ) || null
1724
+ );
1725
+ }
1726
+
1690
1727
  /**
1691
1728
  * Finish the link here, by whatever means work, in cheapest-first order.
1692
1729
  *
@@ -1697,6 +1734,12 @@ function backendRedirect(repo, status, serverConfig) {
1697
1734
  * the config we wrote.
1698
1735
  * 3. Failing that, ask them to restart it and wait, which is all this ever did.
1699
1736
  *
1737
+ * A directory whose repo config redirects the backend moves steps 1 and 2 out of
1738
+ * that directory rather than giving up in it: the config on disk is right, and
1739
+ * where the server and the agent stand is the only thing that has to change. Not
1740
+ * step 2 under `--project`, though — that config exists in that directory alone,
1741
+ * so an agent sent anywhere else would have no server to call.
1742
+ *
1700
1743
  * Every diagnosis is printed the moment it is known rather than saved for the
1701
1744
  * end: the steps below are measured in minutes, and a note that explains what is
1702
1745
  * happening is worth nothing after it has stopped happening.
@@ -1704,8 +1747,16 @@ function backendRedirect(repo, status, serverConfig) {
1704
1747
  * Returns whether the check-in landed, and prints the troubleshooting block
1705
1748
  * itself when it did not.
1706
1749
  */
1707
- async function establishCheckIn(args, agent, apiKey, { serverName, written, serverConfig }) {
1750
+ async function establishCheckIn(
1751
+ args,
1752
+ agent,
1753
+ apiKey,
1754
+ { serverName, written, serverConfig, projectInstall = false }
1755
+ ) {
1708
1756
  const notes = [];
1757
+ // Set only when this directory would redirect the agent, and then it is where
1758
+ // the agent gets started instead.
1759
+ let elsewhere = null;
1709
1760
  const ticker = pollTicker();
1710
1761
  const say = (text) => {
1711
1762
  ticker.end();
@@ -1741,15 +1792,62 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
1741
1792
  const redirect = backendRedirect(repo, status, serverConfig);
1742
1793
  if (redirect) {
1743
1794
  const flag = `--agent ${agent.id.replace("_", "-")}`;
1795
+ const notLinked = (why) =>
1796
+ say(
1797
+ `\nNot linked: ${redirect.path} forces PREMAN_BACKEND=${redirect.actual} for anything\n` +
1798
+ `started in this directory, so ${agent.label} cannot check in against ${redirect.wanted}.\n` +
1799
+ ` - ${why}\n` +
1800
+ ` - Or connect to that backend: ${cliInvocation()} connect ${flag} --backend ${redirect.actual}\n` +
1801
+ ` (needs a key issued by it, and that API running).\n` +
1802
+ ` - Config written to: ${written.path}\n`
1803
+ );
1804
+ notes.push(
1805
+ `start ${agent.label} from another project, or connect to the backend this ` +
1806
+ `directory forces: ${cliInvocation()} connect ${flag} --backend ${redirect.actual} ` +
1807
+ `(needs a key issued by it, and that API running).`
1808
+ );
1809
+
1810
+ // The config we wrote is correct; the directory around it is not. Since the
1811
+ // server only reads the file it is standing in, everything this does next
1812
+ // happens somewhere the override cannot reach.
1813
+ const outside = neutralCwd();
1814
+ if (!outside) {
1815
+ notLinked(`Run '${cliInvocation()} connect' from your own project instead of this directory.`);
1816
+ return false;
1817
+ }
1818
+
1744
1819
  say(
1745
- `\nNot linked: ${redirect.path} forces PREMAN_BACKEND=${redirect.actual} for anything\n` +
1746
- `started in this directory, so ${agent.label} cannot check in against ${redirect.wanted}.\n` +
1747
- ` - Run '${cliInvocation()} connect' from your own project instead of this directory.\n` +
1748
- ` - Or connect to that backend: ${cliInvocation()} connect ${flag} --backend ${redirect.actual}\n` +
1749
- ` (needs a key issued by it, and that API running).\n` +
1750
- ` - Config written to: ${written.path}\n`
1820
+ (projectInstall
1821
+ ? `\nThe key and backend written are right: ${redirect.path}\n` +
1822
+ `only redirects what starts in this directory. `
1823
+ : `\nThe config is written and works anywhere else: ${redirect.path}\n` +
1824
+ `only redirects agents started in this directory. `) +
1825
+ `Trying again from\n${outside}, which is out of that file's reach.\n`
1751
1826
  );
1752
- return false;
1827
+ // Nothing was wrong with the self-test but where it stood, and a self-test
1828
+ // is what completes the link — so re-run it before spending an agent window
1829
+ // on the same question. This works under --project too: it runs the config
1830
+ // in memory rather than loading it from disk.
1831
+ const retry = await mcpSelfTest(serverConfig, { cwd: outside });
1832
+ if (
1833
+ retry.ok &&
1834
+ retry.status?.authenticated &&
1835
+ (await waitForConnection(args, apiKey, { timeoutMs: 15000 }))
1836
+ ) {
1837
+ return true;
1838
+ }
1839
+
1840
+ if (projectInstall) {
1841
+ // --project put the server in this directory and nowhere else, so there
1842
+ // is no config to load anywhere the override cannot reach, and no agent
1843
+ // worth starting to look for one.
1844
+ notLinked(
1845
+ `This --project config exists only here; started anywhere else, ` +
1846
+ `${agent.label} has no ${serverName} server to call.`
1847
+ );
1848
+ return false;
1849
+ }
1850
+ elsewhere = outside;
1753
1851
  }
1754
1852
  }
1755
1853
 
@@ -1758,6 +1856,7 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
1758
1856
  if (!args.has("--no-auto-checkin") && !blocker) {
1759
1857
  launch = await autoCheckIn(args, agent, apiKey, {
1760
1858
  serverName,
1859
+ cwd: elsewhere || process.cwd(),
1761
1860
  onPoll: ticker.tick,
1762
1861
  onLaunch: ({ mode, terminal }) =>
1763
1862
  say(
@@ -1787,7 +1886,11 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
1787
1886
  launch.interactive
1788
1887
  ? `\nAnswer it in the ${agent.label} window — it links on its first PreMan call.\n` +
1789
1888
  "Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
1790
- : `\nRestart ${agent.label} and ask it: "run preman_status"\n` +
1889
+ : // Restarting it here would read the same redirecting file again, so the
1890
+ // one instruction that works is to start it somewhere else.
1891
+ (elsewhere
1892
+ ? `\nStart ${agent.label} in another project and ask it: "run preman_status"\n`
1893
+ : `\nRestart ${agent.label} and ask it: "run preman_status"\n`) +
1791
1894
  "Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
1792
1895
  );
1793
1896
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {