premanmcp 0.10.2 → 0.10.4
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.
- package/README.md +5 -3
- package/bin/connect.js +138 -23
- 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`
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1124,7 +1130,7 @@ export function headlessDiscovery(agent, serverName, instructions = []) {
|
|
|
1124
1130
|
*
|
|
1125
1131
|
* Never throws: a failed discovery must not fail the connect.
|
|
1126
1132
|
*/
|
|
1127
|
-
async function discoverEndpoints(args, agent, serverName) {
|
|
1133
|
+
async function discoverEndpoints(args, agent, serverName, blockedHere = "") {
|
|
1128
1134
|
try {
|
|
1129
1135
|
const before = await endpointCounts(args);
|
|
1130
1136
|
if (before.registered) {
|
|
@@ -1136,7 +1142,10 @@ async function discoverEndpoints(args, agent, serverName) {
|
|
|
1136
1142
|
|
|
1137
1143
|
const brief = await callPremanTool(args, "discover_endpoints_from_codebase", { base_path: "." });
|
|
1138
1144
|
const spec = headlessDiscovery(agent, serverName, brief.instructions || []);
|
|
1139
|
-
|
|
1145
|
+
// Discovery has to read *this* repo, so it cannot be moved out of a directory
|
|
1146
|
+
// that redirects it the way the check-in was — it can only be skipped, before
|
|
1147
|
+
// it spends ten minutes proving what the self-test already found out.
|
|
1148
|
+
const blocker = blockedHere || (spec && onPath(spec.bin) ? agentBlocker(agent.id) : "");
|
|
1140
1149
|
if (!spec || !onPath(spec.bin) || blocker) {
|
|
1141
1150
|
// Say why before printing homework: "here is a brief" reads as PreMan not
|
|
1142
1151
|
// working, when the actual answer is one login away.
|
|
@@ -1436,11 +1445,11 @@ async function setUpPushTesting(args) {
|
|
|
1436
1445
|
* Order follows what a new account needs to see: what PreMan found, then where to
|
|
1437
1446
|
* watch it, then who to tell, then when to run it.
|
|
1438
1447
|
*/
|
|
1439
|
-
async function guidedFirstRun(args, agent, apiKey, serverName) {
|
|
1448
|
+
async function guidedFirstRun(args, agent, apiKey, serverName, { blockedHere = "" } = {}) {
|
|
1440
1449
|
const assumeYes = args.has("--yes");
|
|
1441
1450
|
|
|
1442
1451
|
step("Endpoints");
|
|
1443
|
-
const counts = await discoverEndpoints(args, agent, serverName);
|
|
1452
|
+
const counts = await discoverEndpoints(args, agent, serverName, blockedHere);
|
|
1444
1453
|
|
|
1445
1454
|
if (counts.first) {
|
|
1446
1455
|
step("First test");
|
|
@@ -1655,7 +1664,13 @@ export async function connectCommand(commandArgs) {
|
|
|
1655
1664
|
return;
|
|
1656
1665
|
}
|
|
1657
1666
|
|
|
1658
|
-
|
|
1667
|
+
const checkIn = await establishCheckIn(args, agent, apiKey, {
|
|
1668
|
+
serverName,
|
|
1669
|
+
written,
|
|
1670
|
+
serverConfig,
|
|
1671
|
+
projectInstall,
|
|
1672
|
+
});
|
|
1673
|
+
if (!checkIn.linked) {
|
|
1659
1674
|
// Still honour an explicitly-passed credential, but do not open a new prompt
|
|
1660
1675
|
// on top of a connect that just told the user something went wrong.
|
|
1661
1676
|
await captureDispatchCredential(args, agent, apiKey, { prompt: false });
|
|
@@ -1664,7 +1679,7 @@ export async function connectCommand(commandArgs) {
|
|
|
1664
1679
|
|
|
1665
1680
|
process.stdout.write(`${MARK.ok()} Connected as ${agent.label}.\n`);
|
|
1666
1681
|
if (!args.has("--no-guide")) {
|
|
1667
|
-
await guidedFirstRun(args, agent, apiKey, serverName);
|
|
1682
|
+
await guidedFirstRun(args, agent, apiKey, serverName, { blockedHere: checkIn.blockedHere });
|
|
1668
1683
|
}
|
|
1669
1684
|
await captureDispatchCredential(args, agent, apiKey);
|
|
1670
1685
|
}
|
|
@@ -1687,6 +1702,30 @@ function backendRedirect(repo, status, serverConfig) {
|
|
|
1687
1702
|
return { path: repo.path, actual, wanted };
|
|
1688
1703
|
}
|
|
1689
1704
|
|
|
1705
|
+
/**
|
|
1706
|
+
* Somewhere to start the agent when this directory would redirect its backend.
|
|
1707
|
+
*
|
|
1708
|
+
* The server reads its repo config from the working directory and nowhere else —
|
|
1709
|
+
* no walk up to the parents — so any directory without one of those two files is
|
|
1710
|
+
* already out of the override's reach. Home first, because an agent started there
|
|
1711
|
+
* is in a place the user recognises; a temp dir only if home is itself a repo
|
|
1712
|
+
* carrying a config.
|
|
1713
|
+
*
|
|
1714
|
+
* Null means every candidate was covered, and the caller has nothing to offer but
|
|
1715
|
+
* the instructions.
|
|
1716
|
+
*/
|
|
1717
|
+
export function neutralCwd(candidates = [os.homedir(), os.tmpdir()]) {
|
|
1718
|
+
return (
|
|
1719
|
+
candidates.find(
|
|
1720
|
+
(dir) =>
|
|
1721
|
+
dir &&
|
|
1722
|
+
existsSync(dir) &&
|
|
1723
|
+
!existsSync(path.join(dir, ".cursor", "preman-mcp.config.json")) &&
|
|
1724
|
+
!existsSync(path.join(dir, "preman-mcp.config.json"))
|
|
1725
|
+
) || null
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1690
1729
|
/**
|
|
1691
1730
|
* Finish the link here, by whatever means work, in cheapest-first order.
|
|
1692
1731
|
*
|
|
@@ -1697,15 +1736,33 @@ function backendRedirect(repo, status, serverConfig) {
|
|
|
1697
1736
|
* the config we wrote.
|
|
1698
1737
|
* 3. Failing that, ask them to restart it and wait, which is all this ever did.
|
|
1699
1738
|
*
|
|
1739
|
+
* A directory whose repo config redirects the backend moves steps 1 and 2 out of
|
|
1740
|
+
* that directory rather than giving up in it: the config on disk is right, and
|
|
1741
|
+
* where the server and the agent stand is the only thing that has to change. Not
|
|
1742
|
+
* step 2 under `--project`, though — that config exists in that directory alone,
|
|
1743
|
+
* so an agent sent anywhere else would have no server to call.
|
|
1744
|
+
*
|
|
1700
1745
|
* Every diagnosis is printed the moment it is known rather than saved for the
|
|
1701
1746
|
* end: the steps below are measured in minutes, and a note that explains what is
|
|
1702
1747
|
* happening is worth nothing after it has stopped happening.
|
|
1703
1748
|
*
|
|
1704
|
-
* Returns whether the check-in landed
|
|
1705
|
-
*
|
|
1749
|
+
* Returns whether the check-in landed and, when this directory redirects it, why
|
|
1750
|
+
* nothing started here can use PreMan — the caller has its own agent to run once
|
|
1751
|
+
* this is done. Prints the troubleshooting block itself when the link did not
|
|
1752
|
+
* land.
|
|
1706
1753
|
*/
|
|
1707
|
-
async function establishCheckIn(
|
|
1754
|
+
async function establishCheckIn(
|
|
1755
|
+
args,
|
|
1756
|
+
agent,
|
|
1757
|
+
apiKey,
|
|
1758
|
+
{ serverName, written, serverConfig, projectInstall = false }
|
|
1759
|
+
) {
|
|
1708
1760
|
const notes = [];
|
|
1761
|
+
// Both set only when this directory redirects the agent: where it can be
|
|
1762
|
+
// started instead, and why an agent left standing here cannot use PreMan at all.
|
|
1763
|
+
let elsewhere = null;
|
|
1764
|
+
let blockedHere = "";
|
|
1765
|
+
const done = (linked) => ({ linked, blockedHere });
|
|
1709
1766
|
const ticker = pollTicker();
|
|
1710
1767
|
const say = (text) => {
|
|
1711
1768
|
ticker.end();
|
|
@@ -1730,7 +1787,7 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1730
1787
|
);
|
|
1731
1788
|
}
|
|
1732
1789
|
if (test.ok && status.authenticated && (await waitForConnection(args, apiKey, { timeoutMs: 15000 }))) {
|
|
1733
|
-
return true;
|
|
1790
|
+
return done(true);
|
|
1734
1791
|
}
|
|
1735
1792
|
note(
|
|
1736
1793
|
test.ok
|
|
@@ -1741,15 +1798,68 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1741
1798
|
const redirect = backendRedirect(repo, status, serverConfig);
|
|
1742
1799
|
if (redirect) {
|
|
1743
1800
|
const flag = `--agent ${agent.id.replace("_", "-")}`;
|
|
1801
|
+
const notLinked = (why) =>
|
|
1802
|
+
say(
|
|
1803
|
+
`\nNot linked: ${redirect.path} forces PREMAN_BACKEND=${redirect.actual} for anything\n` +
|
|
1804
|
+
`started in this directory, so ${agent.label} cannot check in against ${redirect.wanted}.\n` +
|
|
1805
|
+
` - ${why}\n` +
|
|
1806
|
+
` - Or connect to that backend: ${cliInvocation()} connect ${flag} --backend ${redirect.actual}\n` +
|
|
1807
|
+
` (needs a key issued by it, and that API running).\n` +
|
|
1808
|
+
` - Config written to: ${written.path}\n`
|
|
1809
|
+
);
|
|
1810
|
+
notes.push(
|
|
1811
|
+
`start ${agent.label} from another project, or connect to the backend this ` +
|
|
1812
|
+
`directory forces: ${cliInvocation()} connect ${flag} --backend ${redirect.actual} ` +
|
|
1813
|
+
`(needs a key issued by it, and that API running).`
|
|
1814
|
+
);
|
|
1815
|
+
// Linking is not the only thing this directory breaks: whatever runs here
|
|
1816
|
+
// next gets the same redirected, unauthenticated server, and the callers
|
|
1817
|
+
// that would start an agent in it need to hear so.
|
|
1818
|
+
blockedHere =
|
|
1819
|
+
`${redirect.path} sends ${agent.label} to ${redirect.actual} in this directory, ` +
|
|
1820
|
+
`where this key is not valid.`;
|
|
1821
|
+
|
|
1822
|
+
// The config we wrote is correct; the directory around it is not. Since the
|
|
1823
|
+
// server only reads the file it is standing in, everything this does next
|
|
1824
|
+
// happens somewhere the override cannot reach.
|
|
1825
|
+
const outside = neutralCwd();
|
|
1826
|
+
if (!outside) {
|
|
1827
|
+
notLinked(`Run '${cliInvocation()} connect' from your own project instead of this directory.`);
|
|
1828
|
+
return done(false);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1744
1831
|
say(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
`
|
|
1832
|
+
(projectInstall
|
|
1833
|
+
? `\nThe key and backend written are right: ${redirect.path}\n` +
|
|
1834
|
+
`only redirects what starts in this directory. `
|
|
1835
|
+
: `\nThe config is written and works anywhere else: ${redirect.path}\n` +
|
|
1836
|
+
`only redirects agents started in this directory. `) +
|
|
1837
|
+
`Trying again from\n${outside}, which is out of that file's reach.\n`
|
|
1751
1838
|
);
|
|
1752
|
-
|
|
1839
|
+
// Nothing was wrong with the self-test but where it stood, and a self-test
|
|
1840
|
+
// is what completes the link — so re-run it before spending an agent window
|
|
1841
|
+
// on the same question. This works under --project too: it runs the config
|
|
1842
|
+
// in memory rather than loading it from disk.
|
|
1843
|
+
const retry = await mcpSelfTest(serverConfig, { cwd: outside });
|
|
1844
|
+
if (
|
|
1845
|
+
retry.ok &&
|
|
1846
|
+
retry.status?.authenticated &&
|
|
1847
|
+
(await waitForConnection(args, apiKey, { timeoutMs: 15000 }))
|
|
1848
|
+
) {
|
|
1849
|
+
return done(true);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (projectInstall) {
|
|
1853
|
+
// --project put the server in this directory and nowhere else, so there
|
|
1854
|
+
// is no config to load anywhere the override cannot reach, and no agent
|
|
1855
|
+
// worth starting to look for one.
|
|
1856
|
+
notLinked(
|
|
1857
|
+
`This --project config exists only here; started anywhere else, ` +
|
|
1858
|
+
`${agent.label} has no ${serverName} server to call.`
|
|
1859
|
+
);
|
|
1860
|
+
return done(false);
|
|
1861
|
+
}
|
|
1862
|
+
elsewhere = outside;
|
|
1753
1863
|
}
|
|
1754
1864
|
}
|
|
1755
1865
|
|
|
@@ -1758,6 +1868,7 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1758
1868
|
if (!args.has("--no-auto-checkin") && !blocker) {
|
|
1759
1869
|
launch = await autoCheckIn(args, agent, apiKey, {
|
|
1760
1870
|
serverName,
|
|
1871
|
+
cwd: elsewhere || process.cwd(),
|
|
1761
1872
|
onPoll: ticker.tick,
|
|
1762
1873
|
onLaunch: ({ mode, terminal }) =>
|
|
1763
1874
|
say(
|
|
@@ -1768,7 +1879,7 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1768
1879
|
});
|
|
1769
1880
|
if (launch.connected) {
|
|
1770
1881
|
ticker.end();
|
|
1771
|
-
return true;
|
|
1882
|
+
return done(true);
|
|
1772
1883
|
}
|
|
1773
1884
|
if (launch.ran && !launch.interactive) {
|
|
1774
1885
|
const why = lastLine(launch.output);
|
|
@@ -1787,13 +1898,17 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1787
1898
|
launch.interactive
|
|
1788
1899
|
? `\nAnswer it in the ${agent.label} window — it links on its first PreMan call.\n` +
|
|
1789
1900
|
"Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
|
|
1790
|
-
:
|
|
1901
|
+
: // Restarting it here would read the same redirecting file again, so the
|
|
1902
|
+
// one instruction that works is to start it somewhere else.
|
|
1903
|
+
(elsewhere
|
|
1904
|
+
? `\nStart ${agent.label} in another project and ask it: "run preman_status"\n`
|
|
1905
|
+
: `\nRestart ${agent.label} and ask it: "run preman_status"\n`) +
|
|
1791
1906
|
"Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
|
|
1792
1907
|
);
|
|
1793
1908
|
|
|
1794
1909
|
if (await waitForConnection(args, apiKey, { onPoll: ticker.tick })) {
|
|
1795
1910
|
ticker.end();
|
|
1796
|
-
return true;
|
|
1911
|
+
return done(true);
|
|
1797
1912
|
}
|
|
1798
1913
|
|
|
1799
1914
|
say(
|
|
@@ -1804,5 +1919,5 @@ async function establishCheckIn(args, agent, apiKey, { serverName, written, serv
|
|
|
1804
1919
|
` - Then ask ${agent.label} to "run preman_status" — it links on its first PreMan call.\n` +
|
|
1805
1920
|
` - Then: ${cliInvocation()} connect --agent ${agent.id.replace("_", "-")}\n`
|
|
1806
1921
|
);
|
|
1807
|
-
return false;
|
|
1922
|
+
return done(false);
|
|
1808
1923
|
}
|